aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml4
-rw-r--r--CMakeLists.txt8
-rw-r--r--ci/common/build.sh3
-rw-r--r--cmake/FindLua.cmake197
-rw-r--r--runtime/doc/eval.txt4
-rw-r--r--runtime/doc/if_lua.txt174
-rw-r--r--runtime/doc/index.txt3
-rw-r--r--runtime/doc/usr_41.txt1
-rw-r--r--runtime/doc/vim_diff.txt18
-rw-r--r--scripts/gencharblob.lua48
-rwxr-xr-xscripts/gendeclarations.lua64
-rw-r--r--scripts/genmsgpack.lua (renamed from scripts/gendispatch.lua)196
-rwxr-xr-xsrc/clint.py5
-rw-r--r--src/nvim/CMakeLists.txt129
-rw-r--r--src/nvim/api/buffer.c45
-rw-r--r--src/nvim/api/private/defs.h34
-rw-r--r--src/nvim/api/private/helpers.c11
-rw-r--r--src/nvim/api/private/helpers.h2
-rw-r--r--src/nvim/api/vim.c59
-rw-r--r--src/nvim/api/window.c8
-rw-r--r--src/nvim/eval.c19
-rw-r--r--src/nvim/eval.lua1
-rw-r--r--src/nvim/eval/decode.c149
-rw-r--r--src/nvim/eval/decode.h1
-rw-r--r--src/nvim/eval/typval_encode.c.h5
-rw-r--r--src/nvim/ex_cmds.lua6
-rw-r--r--src/nvim/ex_cmds2.c11
-rw-r--r--src/nvim/ex_docmd.c13
-rw-r--r--src/nvim/ex_getln.c68
-rw-r--r--src/nvim/func_attr.h5
-rw-r--r--src/nvim/lua/converter.c1186
-rw-r--r--src/nvim/lua/converter.h15
-rw-r--r--src/nvim/lua/executor.c576
-rw-r--r--src/nvim/lua/executor.h25
-rw-r--r--src/nvim/lua/vim.lua2
-rw-r--r--src/nvim/msgpack_rpc/helpers.c29
-rw-r--r--src/nvim/msgpack_rpc/helpers.h7
-rw-r--r--test/functional/api/buffer_spec.lua10
-rw-r--r--test/functional/api/vim_spec.lua11
-rw-r--r--test/functional/api/window_spec.lua7
-rw-r--r--test/functional/helpers.lua4
-rw-r--r--test/functional/lua/api_spec.lua204
-rw-r--r--test/functional/lua/commands_spec.lua164
-rw-r--r--test/functional/lua/luaeval_spec.lua255
-rw-r--r--test/functional/lua/overrides_spec.lua175
-rw-r--r--test/helpers.lua9
-rw-r--r--third-party/cmake/BuildLua.cmake19
47 files changed, 3761 insertions, 228 deletions
diff --git a/.travis.yml b/.travis.yml
index 2d9e06eaf6..5c600ce823 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -66,7 +66,9 @@ matrix:
env: BUILD_32BIT=ON
- os: linux
compiler: clang-3.9
- env: CLANG_SANITIZER=ASAN_UBSAN
+ env: >
+ CLANG_SANITIZER=ASAN_UBSAN
+ CMAKE_FLAGS="$CMAKE_FLAGS -DPREFER_LUAJIT=false"
- os: linux
compiler: clang-3.9
env: CLANG_SANITIZER=TSAN
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 03b13404a5..4d1c0e8fd5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -309,6 +309,14 @@ include_directories(SYSTEM ${LIBUV_INCLUDE_DIRS})
find_package(Msgpack 1.0.0 REQUIRED)
include_directories(SYSTEM ${MSGPACK_INCLUDE_DIRS})
+option(PREFER_LUAJIT "Prefer LuaJIT over Lua when compiling executable. Test library always uses luajit." ON)
+
+find_package(LuaJit REQUIRED)
+
+if(NOT PREFER_LUAJIT)
+ find_package(Lua REQUIRED)
+endif()
+
list(APPEND CMAKE_REQUIRED_INCLUDES "${MSGPACK_INCLUDE_DIRS}")
check_c_source_compiles("
#include <msgpack.h>
diff --git a/ci/common/build.sh b/ci/common/build.sh
index 8c2406698a..f398a1a1cc 100644
--- a/ci/common/build.sh
+++ b/ci/common/build.sh
@@ -10,7 +10,8 @@ build_deps() {
if test "${BUILD_32BIT}" = ON ; then
DEPS_CMAKE_FLAGS="${DEPS_CMAKE_FLAGS} ${CMAKE_FLAGS_32BIT}"
fi
- if test "${FUNCTIONALTEST}" = "functionaltest-lua" ; then
+ if test "${FUNCTIONALTEST}" = "functionaltest-lua" \
+ || test "${CLANG_SANITIZER}" = "ASAN_UBSAN" ; then
DEPS_CMAKE_FLAGS="${DEPS_CMAKE_FLAGS} -DUSE_BUNDLED_LUA=ON"
fi
diff --git a/cmake/FindLua.cmake b/cmake/FindLua.cmake
new file mode 100644
index 0000000000..b669a49f29
--- /dev/null
+++ b/cmake/FindLua.cmake
@@ -0,0 +1,197 @@
+# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
+# file Copyright.txt or https://cmake.org/licensing for details.
+
+#.rst:
+# FindLua
+# -------
+#
+#
+#
+# Locate Lua library This module defines
+#
+# ::
+#
+# LUA_FOUND - if false, do not try to link to Lua
+# LUA_LIBRARIES - both lua and lualib
+# LUA_INCLUDE_DIR - where to find lua.h
+# LUA_VERSION_STRING - the version of Lua found
+# LUA_VERSION_MAJOR - the major version of Lua
+# LUA_VERSION_MINOR - the minor version of Lua
+# LUA_VERSION_PATCH - the patch version of Lua
+#
+#
+#
+# Note that the expected include convention is
+#
+# ::
+#
+# #include "lua.h"
+#
+# and not
+#
+# ::
+#
+# #include <lua/lua.h>
+#
+# This is because, the lua location is not standardized and may exist in
+# locations other than lua/
+
+unset(_lua_include_subdirs)
+unset(_lua_library_names)
+unset(_lua_append_versions)
+
+# this is a function only to have all the variables inside go away automatically
+function(_lua_set_version_vars)
+ set(LUA_VERSIONS5 5.3 5.2 5.1 5.0)
+
+ if (Lua_FIND_VERSION_EXACT)
+ if (Lua_FIND_VERSION_COUNT GREATER 1)
+ set(_lua_append_versions ${Lua_FIND_VERSION_MAJOR}.${Lua_FIND_VERSION_MINOR})
+ endif ()
+ elseif (Lua_FIND_VERSION)
+ # once there is a different major version supported this should become a loop
+ if (NOT Lua_FIND_VERSION_MAJOR GREATER 5)
+ if (Lua_FIND_VERSION_COUNT EQUAL 1)
+ set(_lua_append_versions ${LUA_VERSIONS5})
+ else ()
+ foreach (subver IN LISTS LUA_VERSIONS5)
+ if (NOT subver VERSION_LESS ${Lua_FIND_VERSION})
+ list(APPEND _lua_append_versions ${subver})
+ endif ()
+ endforeach ()
+ endif ()
+ endif ()
+ else ()
+ # once there is a different major version supported this should become a loop
+ set(_lua_append_versions ${LUA_VERSIONS5})
+ endif ()
+
+ list(APPEND _lua_include_subdirs "include/lua" "include")
+
+ foreach (ver IN LISTS _lua_append_versions)
+ string(REGEX MATCH "^([0-9]+)\\.([0-9]+)$" _ver "${ver}")
+ list(APPEND _lua_include_subdirs
+ include/lua${CMAKE_MATCH_1}${CMAKE_MATCH_2}
+ include/lua${CMAKE_MATCH_1}.${CMAKE_MATCH_2}
+ include/lua-${CMAKE_MATCH_1}.${CMAKE_MATCH_2}
+ )
+ endforeach ()
+
+ set(_lua_include_subdirs "${_lua_include_subdirs}" PARENT_SCOPE)
+ set(_lua_append_versions "${_lua_append_versions}" PARENT_SCOPE)
+endfunction(_lua_set_version_vars)
+
+function(_lua_check_header_version _hdr_file)
+ # At least 5.[012] have different ways to express the version
+ # so all of them need to be tested. Lua 5.2 defines LUA_VERSION
+ # and LUA_RELEASE as joined by the C preprocessor, so avoid those.
+ file(STRINGS "${_hdr_file}" lua_version_strings
+ REGEX "^#define[ \t]+LUA_(RELEASE[ \t]+\"Lua [0-9]|VERSION([ \t]+\"Lua [0-9]|_[MR])).*")
+
+ string(REGEX REPLACE ".*;#define[ \t]+LUA_VERSION_MAJOR[ \t]+\"([0-9])\"[ \t]*;.*" "\\1" LUA_VERSION_MAJOR ";${lua_version_strings};")
+ if (LUA_VERSION_MAJOR MATCHES "^[0-9]+$")
+ string(REGEX REPLACE ".*;#define[ \t]+LUA_VERSION_MINOR[ \t]+\"([0-9])\"[ \t]*;.*" "\\1" LUA_VERSION_MINOR ";${lua_version_strings};")
+ string(REGEX REPLACE ".*;#define[ \t]+LUA_VERSION_RELEASE[ \t]+\"([0-9])\"[ \t]*;.*" "\\1" LUA_VERSION_PATCH ";${lua_version_strings};")
+ set(LUA_VERSION_STRING "${LUA_VERSION_MAJOR}.${LUA_VERSION_MINOR}.${LUA_VERSION_PATCH}")
+ else ()
+ string(REGEX REPLACE ".*;#define[ \t]+LUA_RELEASE[ \t]+\"Lua ([0-9.]+)\"[ \t]*;.*" "\\1" LUA_VERSION_STRING ";${lua_version_strings};")
+ if (NOT LUA_VERSION_STRING MATCHES "^[0-9.]+$")
+ string(REGEX REPLACE ".*;#define[ \t]+LUA_VERSION[ \t]+\"Lua ([0-9.]+)\"[ \t]*;.*" "\\1" LUA_VERSION_STRING ";${lua_version_strings};")
+ endif ()
+ string(REGEX REPLACE "^([0-9]+)\\.[0-9.]*$" "\\1" LUA_VERSION_MAJOR "${LUA_VERSION_STRING}")
+ string(REGEX REPLACE "^[0-9]+\\.([0-9]+)[0-9.]*$" "\\1" LUA_VERSION_MINOR "${LUA_VERSION_STRING}")
+ string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.([0-9]).*" "\\1" LUA_VERSION_PATCH "${LUA_VERSION_STRING}")
+ endif ()
+ foreach (ver IN LISTS _lua_append_versions)
+ if (ver STREQUAL "${LUA_VERSION_MAJOR}.${LUA_VERSION_MINOR}")
+ set(LUA_VERSION_MAJOR ${LUA_VERSION_MAJOR} PARENT_SCOPE)
+ set(LUA_VERSION_MINOR ${LUA_VERSION_MINOR} PARENT_SCOPE)
+ set(LUA_VERSION_PATCH ${LUA_VERSION_PATCH} PARENT_SCOPE)
+ set(LUA_VERSION_STRING ${LUA_VERSION_STRING} PARENT_SCOPE)
+ return()
+ endif ()
+ endforeach ()
+endfunction(_lua_check_header_version)
+
+_lua_set_version_vars()
+
+if (LUA_INCLUDE_DIR AND EXISTS "${LUA_INCLUDE_DIR}/lua.h")
+ _lua_check_header_version("${LUA_INCLUDE_DIR}/lua.h")
+endif ()
+
+if (NOT LUA_VERSION_STRING)
+ foreach (subdir IN LISTS _lua_include_subdirs)
+ unset(LUA_INCLUDE_PREFIX CACHE)
+ find_path(LUA_INCLUDE_PREFIX ${subdir}/lua.h
+ HINTS
+ ENV LUA_DIR
+ PATHS
+ ~/Library/Frameworks
+ /Library/Frameworks
+ /sw # Fink
+ /opt/local # DarwinPorts
+ /opt/csw # Blastwave
+ /opt
+ )
+ if (LUA_INCLUDE_PREFIX)
+ _lua_check_header_version("${LUA_INCLUDE_PREFIX}/${subdir}/lua.h")
+ if (LUA_VERSION_STRING)
+ set(LUA_INCLUDE_DIR "${LUA_INCLUDE_PREFIX}/${subdir}")
+ break()
+ endif ()
+ endif ()
+ endforeach ()
+endif ()
+unset(_lua_include_subdirs)
+unset(_lua_append_versions)
+
+if (LUA_VERSION_STRING)
+ set(_lua_library_names
+ lua${LUA_VERSION_MAJOR}${LUA_VERSION_MINOR}
+ lua${LUA_VERSION_MAJOR}.${LUA_VERSION_MINOR}
+ lua-${LUA_VERSION_MAJOR}.${LUA_VERSION_MINOR}
+ lua.${LUA_VERSION_MAJOR}.${LUA_VERSION_MINOR}
+ )
+endif ()
+
+find_library(LUA_LIBRARY
+ NAMES ${_lua_library_names} lua
+ HINTS
+ ENV LUA_DIR
+ PATH_SUFFIXES lib
+ PATHS
+ ~/Library/Frameworks
+ /Library/Frameworks
+ /sw
+ /opt/local
+ /opt/csw
+ /opt
+)
+unset(_lua_library_names)
+
+if (LUA_LIBRARY)
+ # include the math library for Unix
+ if (UNIX AND NOT APPLE AND NOT BEOS)
+ find_library(LUA_MATH_LIBRARY m)
+ set(LUA_LIBRARIES "${LUA_LIBRARY};${LUA_MATH_LIBRARY}")
+
+ # include dl library for statically-linked Lua library
+ get_filename_component(LUA_LIB_EXT ${LUA_LIBRARY} EXT)
+ if(LUA_LIB_EXT STREQUAL CMAKE_STATIC_LIBRARY_SUFFIX)
+ list(APPEND LUA_LIBRARIES ${CMAKE_DL_LIBS})
+ endif()
+
+ # For Windows and Mac, don't need to explicitly include the math library
+ else ()
+ set(LUA_LIBRARIES "${LUA_LIBRARY}")
+ endif ()
+endif ()
+
+include(FindPackageHandleStandardArgs)
+# handle the QUIETLY and REQUIRED arguments and set LUA_FOUND to TRUE if
+# all listed variables are TRUE
+FIND_PACKAGE_HANDLE_STANDARD_ARGS(Lua
+ REQUIRED_VARS LUA_LIBRARIES LUA_INCLUDE_DIR
+ VERSION_VAR LUA_VERSION_STRING)
+
+mark_as_advanced(LUA_INCLUDE_DIR LUA_LIBRARY LUA_MATH_LIBRARY)
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt
index 3b9c11770e..911fe43819 100644
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -2136,6 +2136,7 @@ lispindent({lnum}) Number Lisp indent for line {lnum}
localtime() Number current time
log({expr}) Float natural logarithm (base e) of {expr}
log10({expr}) Float logarithm of Float {expr} to base 10
+luaeval({expr}[, {expr}]) any evaluate Lua expression
map({expr1}, {expr2}) List/Dict change each item in {expr1} to {expr}
maparg({name}[, {mode} [, {abbr} [, {dict}]]])
String or Dict
@@ -5109,6 +5110,9 @@ log10({expr}) *log10()*
:echo log10(0.01)
< -2.0
+luaeval({expr}[, {expr}])
+ Evaluate Lua expression {expr} and return its result converted
+ to Vim data structures. See |lua-luaeval| for more details.
map({expr1}, {expr2}) *map()*
{expr1} must be a |List| or a |Dictionary|.
diff --git a/runtime/doc/if_lua.txt b/runtime/doc/if_lua.txt
new file mode 100644
index 0000000000..932564170d
--- /dev/null
+++ b/runtime/doc/if_lua.txt
@@ -0,0 +1,174 @@
+*if_lua.txt* For Neovim
+
+
+ VIM REFERENCE MANUAL by Luis Carvalho
+
+
+The Lua Interface to Vim *lua* *Lua*
+
+1. Commands |lua-commands|
+2. The vim module |lua-vim|
+3. The luaeval function |lua-luaeval|
+
+==============================================================================
+1. Commands *lua-commands*
+
+ *:lua*
+:[range]lua {chunk}
+ Execute Lua chunk {chunk}. {not in Vi}
+
+Examples:
+>
+ :lua vim.api.nvim_command('echo "Hello, Neovim!"')
+<
+
+:[range]lua << {endmarker}
+{script}
+{endmarker}
+ Execute Lua script {script}. {not in Vi}
+ Note: This command doesn't work when the Lua
+ feature wasn't compiled in. To avoid errors, see
+ |script-here|.
+
+{endmarker} must NOT be preceded by any white space. If {endmarker} is
+omitted from after the "<<", a dot '.' must be used after {script}, like
+for the |:append| and |:insert| commands.
+This form of the |:lua| command is mainly useful for including Lua code
+in Vim scripts.
+
+Example:
+>
+ function! CurrentLineInfo()
+ lua << EOF
+ local linenr = vim.api.nvim_win_get_cursor(0)[1]
+ local curline = vim.api.nvim_buf_get_lines(
+ 0, linenr, linenr + 1, false)[1]
+ print(string.format("Current line [%d] has %d bytes",
+ linenr, #curline))
+ EOF
+ endfunction
+
+Note that in example variables are prefixed with local: they will disappear
+when block finishes. This is not the case for globals.
+
+To see what version of Lua you have: >
+ :lua print(_VERSION)
+
+If you use LuaJIT you can also use this: >
+ :lua print(jit.version)
+<
+
+ *:luado*
+:[range]luado {body} Execute Lua function "function (line, linenr) {body}
+ end" for each line in the [range], with the function
+ argument being set to the text of each line in turn,
+ without a trailing <EOL>, and the current line number.
+ If the value returned by the function is a string it
+ becomes the text of the line in the current turn. The
+ default for [range] is the whole file: "1,$".
+ {not in Vi}
+
+Examples:
+>
+ :luado return string.format("%s\t%d", line:reverse(), #line)
+
+ :lua require"lpeg"
+ :lua -- balanced parenthesis grammar:
+ :lua bp = lpeg.P{ "(" * ((1 - lpeg.S"()") + lpeg.V(1))^0 * ")" }
+ :luado if bp:match(line) then return "-->\t" .. line end
+<
+
+ *:luafile*
+:[range]luafile {file}
+ Execute Lua script in {file}. {not in Vi}
+ The whole argument is used as a single file name.
+
+Examples:
+>
+ :luafile script.lua
+ :luafile %
+<
+
+All these commands execute a Lua chunk from either the command line (:lua and
+:luado) or a file (:luafile) with the given line [range]. Similarly to the Lua
+interpreter, each chunk has its own scope and so only global variables are
+shared between command calls. All Lua default libraries are available. In
+addition, Lua "print" function has its output redirected to the Vim message
+area, with arguments separated by a white space instead of a tab.
+
+Lua uses the "vim" module (see |lua-vim|) to issue commands to Neovim
+and manage buffers (|lua-buffer|) and windows (|lua-window|). However,
+procedures that alter buffer content, open new buffers, and change cursor
+position are restricted when the command is executed in the |sandbox|.
+
+
+==============================================================================
+2. The vim module *lua-vim*
+
+Lua interfaces Vim through the "vim" module. Currently it only has `api`
+submodule which is a table with all API functions. Descriptions of these
+functions may be found in |api-funcs.txt|.
+
+==============================================================================
+3. The luaeval function *lua-luaeval* *lua-eval*
+ *luaeval()*
+
+The (dual) equivalent of "vim.eval" for passing Lua values to Vim is
+"luaeval". "luaeval" takes an expression string and an optional argument used
+for _A inside expression and returns the result of the expression. It is
+semantically equivalent in Lua to:
+>
+ local chunkheader = "local _A = select(1, ...) return "
+ function luaeval (expstr, arg)
+ local chunk = assert(loadstring(chunkheader .. expstr, "luaeval"))
+ return chunk(arg) -- return typval
+ end
+
+Note that "_A" receives the argument to "luaeval". Lua nils, numbers, strings,
+tables and booleans are converted to their Vim respective types. An error is
+thrown if conversion of any of the remaining Lua types is attempted.
+
+Note 2: lua tables are used as both dictionaries and lists, thus making it
+impossible to determine whether empty table is meant to be empty list or empty
+dictionary. Additionally lua does not have integer numbers. To distinguish
+between these cases there is the following agreement:
+
+0. Empty table is empty list.
+1. Table with N incrementally growing integral numbers, starting from 1 and
+ ending with N is considered to be a list.
+2. Table with string keys, none of which contains NUL byte, is considered to
+ be a dictionary.
+3. Table with string keys, at least one of which contains NUL byte, is also
+ considered to be a dictionary, but this time it is converted to
+ a |msgpack-special-map|.
+4. Table with `vim.type_idx` key may be a dictionary, a list or floating-point
+ value:
+ - `{[vim.type_idx]=vim.types.float, [vim.val_idx]=1}` is converted to
+ a floating-point 1.0. Note that by default integral lua numbers are
+ converted to |Number|s, non-integral are converted to |Float|s. This
+ variant allows integral |Float|s.
+ - `{[vim.type_idx]=vim.types.dictionary}` is converted to an empty
+ dictionary, `{[vim.type_idx]=vim.types.dictionary, [42]=1, a=2}` is
+ converted to a dictionary `{'a': 42}`: non-string keys are ignored.
+ Without `vim.type_idx` key tables with keys not fitting in 1., 2. or 3.
+ are errors.
+ - `{[vim.type_idx]=vim.types.list}` is converted to an empty list. As well
+ as `{[vim.type_idx]=vim.types.list, [42]=1}`: integral keys that do not
+ form a 1-step sequence from 1 to N are ignored, as well as all
+ non-integral keys.
+
+Examples: >
+
+ :echo luaeval('math.pi')
+ :function Rand(x,y) " random uniform between x and y
+ : return luaeval('(_A.y-_A.x)*math.random()+_A.x', {'x':a:x,'y':a:y})
+ : endfunction
+ :echo Rand(1,10)
+
+Note that currently second argument to `luaeval` undergoes VimL to lua
+conversion, so changing containers in lua do not affect values in VimL. Return
+value is also always converted. When converting, |msgpack-special-dict|s are
+treated specially.
+
+==============================================================================
+ vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/runtime/doc/index.txt b/runtime/doc/index.txt
index 06196dbefa..c15587cffd 100644
--- a/runtime/doc/index.txt
+++ b/runtime/doc/index.txt
@@ -1331,6 +1331,9 @@ tag command action ~
|:ltag| :lt[ag] jump to tag and add matching tags to the
location list
|:lunmap| :lu[nmap] like ":unmap!" but includes Lang-Arg mode
+|:lua| :lua execute Lua command
+|:luado| :luad[o] execute Lua command for each line
+|:luafile| :luaf[ile] execute Lua script file
|:lvimgrep| :lv[imgrep] search for pattern in files
|:lvimgrepadd| :lvimgrepa[dd] like :vimgrep, but append to current list
|:lwindow| :lw[indow] open or close location window
diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index 1d09b532a4..acb6fd0fa4 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -950,6 +950,7 @@ Various: *various-functions*
taglist() get list of matching tags
tagfiles() get a list of tags files
+ luaeval() evaluate Lua expression
py3eval() evaluate Python expression (|+python3|)
pyeval() evaluate Python expression (|+python|)
pyxeval() evaluate |python_x| expression
diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt
index de2dd601c5..e6184fd528 100644
--- a/runtime/doc/vim_diff.txt
+++ b/runtime/doc/vim_diff.txt
@@ -214,22 +214,15 @@ Additional differences:
- |shada-c| has no effect.
- |shada-s| now limits size of every item and not just registers.
-- When reading ShaDa files items are merged according to the timestamp.
- |shada-merging|
- 'viminfo' option got renamed to 'shada'. Old option is kept as an alias for
compatibility reasons.
- |:wviminfo| was renamed to |:wshada|, |:rviminfo| to |:rshada|. Old
commands are still kept.
-- When writing (|:wshada| without bang or at exit) it merges much more data,
- and does this according to the timestamp. Vim merges only marks.
- |shada-merging|
- ShaDa file format was designed with forward and backward compatibility in
mind. |shada-compatibility|
- Some errors make ShaDa code keep temporary file in-place for user to decide
what to do with it. Vim deletes temporary file in these cases.
|shada-error-handling|
-- Vim keeps no timestamps at all, neither in viminfo file nor in the instance
- itself.
- ShaDa file keeps search direction (|v:searchforward|), viminfo does not.
|printf()| returns something meaningful when used with `%p` argument: in Vim
@@ -240,6 +233,17 @@ coerced to strings. See |id()| for more details, currently it uses
|c_CTRL-R| pasting a non-special register into |cmdline| omits the last <CR>.
+Lua interface (|if_lua.txt|):
+
+- `:lua print("a\0b")` will print `a^@b`, like with `:echomsg "a\nb"` . In Vim
+ that prints `a` and `b` on separate lines, exactly like
+ `:lua print("a\nb")` .
+- `:lua error('TEST')` will print “TEST” as the error in Vim and “E5105: Error
+ while calling lua chunk: [string "<VimL compiled string>"]:1: TEST” in
+ Neovim.
+- Lua has direct access to Neovim api via `vim.api`.
+- Currently most of features are missing.
+
==============================================================================
5. Missing legacy features *nvim-features-missing*
*if_lua* *if_perl* *if_mzscheme* *if_tcl*
diff --git a/scripts/gencharblob.lua b/scripts/gencharblob.lua
new file mode 100644
index 0000000000..d860375e26
--- /dev/null
+++ b/scripts/gencharblob.lua
@@ -0,0 +1,48 @@
+if arg[1] == '--help' then
+ print('Usage:')
+ print(' gencharblob.lua source target varname')
+ print('')
+ print('Generates C file with big uint8_t blob.')
+ print('Blob will be stored in a static const array named varname.')
+ os.exit()
+end
+
+assert(#arg == 3)
+
+local source_file = arg[1]
+local target_file = arg[2]
+local varname = arg[3]
+
+source = io.open(source_file, 'r')
+target = io.open(target_file, 'w')
+
+target:write('#include <stdint.h>\n\n')
+target:write(('static const uint8_t %s[] = {\n'):format(varname))
+
+num_bytes = 0
+MAX_NUM_BYTES = 15 -- 78 / 5: maximum number of bytes on one line
+target:write(' ')
+
+increase_num_bytes = function()
+ num_bytes = num_bytes + 1
+ if num_bytes == MAX_NUM_BYTES then
+ num_bytes = 0
+ target:write('\n ')
+ end
+end
+
+for line in source:lines() do
+ for i = 1,string.len(line) do
+ byte = string.byte(line, i)
+ assert(byte ~= 0)
+ target:write(string.format(' %3u,', byte))
+ increase_num_bytes()
+ end
+ target:write(string.format(' %3u,', string.byte('\n', 1)))
+ increase_num_bytes()
+end
+
+target:write(' 0};\n')
+
+source:close()
+target:close()
diff --git a/scripts/gendeclarations.lua b/scripts/gendeclarations.lua
index ff69b18ae4..e999e53e4a 100755
--- a/scripts/gendeclarations.lua
+++ b/scripts/gendeclarations.lua
@@ -69,17 +69,18 @@ local word = branch(
right_word
)
)
+local inline_comment = concat(
+ lit('/*'),
+ any_amount(concat(
+ neg_look_ahead(lit('*/')),
+ any_character
+ )),
+ lit('*/')
+)
local spaces = any_amount(branch(
s,
-- Comments are really handled by preprocessor, so the following is not needed
- concat(
- lit('/*'),
- any_amount(concat(
- neg_look_ahead(lit('*/')),
- any_character
- )),
- lit('*/')
- ),
+ inline_comment,
concat(
lit('//'),
any_amount(concat(
@@ -110,6 +111,7 @@ local typ = one_or_more(typ_part)
local typ_id = two_or_more(typ_part)
local arg = typ_id -- argument name is swallowed by typ
local pattern = concat(
+ any_amount(branch(set(' ', '\t'), inline_comment)),
typ_id, -- return type with function name
spaces,
lit('('),
@@ -188,24 +190,44 @@ local footer = [[
local non_static = header
local static = header
-local filepattern = '^#%a* %d+ "[^"]-/?([^"/]+)"'
+local filepattern = '^#%a* (%d+) "([^"]-)/?([^"/]+)"'
local curfile
-init = 0
-curfile = nil
-neededfile = fname:match('[^/]+$')
+local init = 0
+local curfile = nil
+local neededfile = fname:match('[^/]+$')
+local declline = 0
+local declendpos = 0
+local curdir = nil
+local is_needed_file = false
while init ~= nil do
- init = text:find('\n', init)
+ init = text:find('[\n;}]', init)
if init == nil then
break
end
+ local init_is_nl = text:sub(init, init) == '\n'
init = init + 1
- if text:sub(init, init) == '#' then
- file = text:match(filepattern, init)
+ if init_is_nl and is_needed_file then
+ declline = declline + 1
+ end
+ if init_is_nl and text:sub(init, init) == '#' then
+ local line, dir, file = text:match(filepattern, init)
if file ~= nil then
curfile = file
+ is_needed_file = (curfile == neededfile)
+ declline = tonumber(line) - 1
+ local curdir_start = dir:find('src/nvim/')
+ if curdir_start ~= nil then
+ curdir = dir:sub(curdir_start + #('src/nvim/'))
+ else
+ curdir = dir
+ end
+ else
+ declline = declline - 1
end
- elseif curfile == neededfile then
+ elseif init < declendpos then
+ -- Skipping over declaration
+ elseif is_needed_file then
s = init
e = pattern:match(text, init)
if e ~= nil then
@@ -225,13 +247,17 @@ while init ~= nil do
declaration = declaration:gsub(' ?(%*+) ?', ' %1')
declaration = declaration:gsub(' ?(FUNC_ATTR_)', ' %1')
declaration = declaration:gsub(' $', '')
- declaration = declaration .. ';\n'
- if text:sub(s, s + 5) == 'static' then
+ declaration = declaration:gsub('^ ', '')
+ declaration = declaration .. ';'
+ declaration = declaration .. (' // %s/%s:%u'):format(
+ curdir, curfile, declline)
+ declaration = declaration .. '\n'
+ if declaration:sub(1, 6) == 'static' then
static = static .. declaration
else
non_static = non_static .. declaration
end
- init = e
+ declendpos = e
end
end
end
diff --git a/scripts/gendispatch.lua b/scripts/genmsgpack.lua
index c0291c55d3..86a051fb4c 100644
--- a/scripts/gendispatch.lua
+++ b/scripts/genmsgpack.lua
@@ -47,7 +47,16 @@ c_proto = Ct(
grammar = Ct((c_proto + c_comment + c_preproc + ws) ^ 1)
-- we need at least 4 arguments since the last two are output files
-assert(#arg >= 3)
+if arg[1] == '--help' then
+ print('Usage: genmsgpack.lua args')
+ print('Args: 1: source directory')
+ print(' 2: dispatch output file (dispatch_wrappers.generated.h)')
+ print(' 3: functions metadata output file (funcs_metadata.generated.h)')
+ print(' 4: API metadata output file (api_metadata.mpack)')
+ print(' 5: lua C bindings output file (msgpack_lua_c_bindings.generated.c)')
+ print(' rest: C files where API functions are defined')
+end
+assert(#arg >= 4)
functions = {}
local nvimsrcdir = arg[1]
@@ -58,17 +67,18 @@ package.path = nvimsrcdir .. '/?.lua;' .. package.path
headers = {}
-- output h file with generated dispatch functions
-dispatch_outputf = arg[#arg-2]
+dispatch_outputf = arg[2]
-- output h file with packed metadata
-funcs_metadata_outputf = arg[#arg-1]
+funcs_metadata_outputf = arg[3]
-- output metadata mpack file, for use by other build scripts
-mpack_outputf = arg[#arg]
+mpack_outputf = arg[4]
+lua_c_bindings_outputf = arg[5]
-- set of function names, used to detect duplicates
function_names = {}
-- read each input file, parse and append to the api metadata
-for i = 2, #arg - 3 do
+for i = 6, #arg do
local full_path = arg[i]
local parts = {}
for part in string.gmatch(full_path, '[^/]+') do
@@ -119,7 +129,9 @@ local deprecated_aliases = require("api.dispatch_deprecated")
for i,f in ipairs(shallowcopy(functions)) do
local ismethod = false
if startswith(f.name, "nvim_") then
- if f.since == nil then
+ if startswith(f.name, "nvim__") then
+ f.since = -1
+ elseif f.since == nil then
print("Function "..f.name.." lacks since field.\n")
os.exit(1)
end
@@ -170,11 +182,13 @@ exported_attributes = {'name', 'parameters', 'return_type', 'method',
'since', 'deprecated_since'}
exported_functions = {}
for _,f in ipairs(functions) do
- local f_exported = {}
- for _,attr in ipairs(exported_attributes) do
- f_exported[attr] = f[attr]
+ if not startswith(f.name, "nvim__") then
+ local f_exported = {}
+ for _,attr in ipairs(exported_attributes) do
+ f_exported[attr] = f[attr]
+ end
+ exported_functions[#exported_functions+1] = f_exported
end
- exported_functions[#exported_functions+1] = f_exported
end
@@ -212,6 +226,14 @@ local function real_type(type)
return rv
end
+local function attr_name(rt)
+ if rt == 'Float' then
+ return 'floating'
+ else
+ return rt:lower()
+ end
+end
+
-- start the handler functions. Visit each function metadata to build the
-- handler function with code generated for validating arguments and calling to
-- the real API.
@@ -248,7 +270,7 @@ for i = 1, #functions do
output:write('\n '..converted..' = (handle_T)args.items['..(j - 1)..'].data.integer;')
else
output:write('\n if (args.items['..(j - 1)..'].type == kObjectType'..rt..') {')
- output:write('\n '..converted..' = args.items['..(j - 1)..'].data.'..rt:lower()..';')
+ output:write('\n '..converted..' = args.items['..(j - 1)..'].data.'..attr_name(rt)..';')
end
if rt:match('^Buffer$') or rt:match('^Window$') or rt:match('^Tabpage$') or rt:match('^Boolean$') then
-- accept nonnegative integers for Booleans, Buffers, Windows and Tabpages
@@ -336,3 +358,155 @@ output:close()
mpack_output = io.open(mpack_outputf, 'wb')
mpack_output:write(mpack.pack(functions))
mpack_output:close()
+
+local function include_headers(output, headers)
+ for i = 1, #headers do
+ if headers[i]:sub(-12) ~= '.generated.h' then
+ output:write('\n#include "nvim/'..headers[i]..'"')
+ end
+ end
+end
+
+local function write_shifted_output(output, str)
+ str = str:gsub('\n ', '\n')
+ str = str:gsub('^ ', '')
+ str = str:gsub(' +$', '')
+ output:write(str)
+end
+
+-- start building lua output
+output = io.open(lua_c_bindings_outputf, 'wb')
+
+output:write([[
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+
+#include "nvim/func_attr.h"
+#include "nvim/api/private/defs.h"
+#include "nvim/api/private/helpers.h"
+#include "nvim/lua/converter.h"
+]])
+include_headers(output, headers)
+output:write('\n')
+
+lua_c_functions = {}
+
+local function process_function(fn)
+ lua_c_function_name = ('nlua_msgpack_%s'):format(fn.name)
+ write_shifted_output(output, string.format([[
+
+ static int %s(lua_State *lstate)
+ {
+ Error err = ERROR_INIT;
+ if (lua_gettop(lstate) != %i) {
+ api_set_error(&err, kErrorTypeValidation, "Expected %i argument%s");
+ goto exit_0;
+ }
+ ]], lua_c_function_name, #fn.parameters, #fn.parameters,
+ (#fn.parameters == 1) and '' or 's'))
+ lua_c_functions[#lua_c_functions + 1] = {
+ binding=lua_c_function_name,
+ api=fn.name
+ }
+ local cparams = ''
+ local free_code = {}
+ for j = #fn.parameters,1,-1 do
+ param = fn.parameters[j]
+ cparam = string.format('arg%u', j)
+ param_type = real_type(param[1])
+ lc_param_type = param_type:lower()
+ write_shifted_output(output, string.format([[
+ const %s %s = nlua_pop_%s(lstate, &err);
+
+ if (ERROR_SET(&err)) {
+ goto exit_%u;
+ }
+ ]], param[1], cparam, param_type, #fn.parameters - j))
+ free_code[#free_code + 1] = ('api_free_%s(%s);'):format(
+ lc_param_type, cparam)
+ cparams = cparam .. ', ' .. cparams
+ end
+ if fn.receives_channel_id then
+ cparams = 'LUA_INTERNAL_CALL, ' .. cparams
+ end
+ if fn.can_fail then
+ cparams = cparams .. '&err'
+ else
+ cparams = cparams:gsub(', $', '')
+ end
+ local free_at_exit_code = ''
+ for i = 1, #free_code do
+ local rev_i = #free_code - i + 1
+ local code = free_code[rev_i]
+ if i == 1 then
+ free_at_exit_code = free_at_exit_code .. ('\n %s'):format(code)
+ else
+ free_at_exit_code = free_at_exit_code .. ('\n exit_%u:\n %s'):format(
+ rev_i, code)
+ end
+ end
+ local err_throw_code = [[
+
+ exit_0:
+ if (ERROR_SET(&err)) {
+ luaL_where(lstate, 1);
+ lua_pushstring(lstate, err.msg);
+ api_clear_error(&err);
+ lua_concat(lstate, 2);
+ return lua_error(lstate);
+ }
+ ]]
+ if fn.return_type ~= 'void' then
+ if fn.return_type:match('^ArrayOf') then
+ return_type = 'Array'
+ else
+ return_type = fn.return_type
+ end
+ write_shifted_output(output, string.format([[
+ const %s ret = %s(%s);
+ nlua_push_%s(lstate, ret);
+ api_free_%s(ret);
+ %s
+ %s
+ return 1;
+ ]], fn.return_type, fn.name, cparams, return_type, return_type:lower(),
+ free_at_exit_code, err_throw_code))
+ else
+ write_shifted_output(output, string.format([[
+ %s(%s);
+ %s
+ %s
+ return 0;
+ ]], fn.name, cparams, free_at_exit_code, err_throw_code))
+ end
+ write_shifted_output(output, [[
+ }
+ ]])
+end
+
+for _, fn in ipairs(functions) do
+ if not fn.noeval or fn.name:sub(1, 4) == '_vim' then
+ process_function(fn)
+ end
+end
+
+output:write(string.format([[
+void nlua_add_api_functions(lua_State *lstate)
+ FUNC_ATTR_NONNULL_ALL
+{
+ lua_createtable(lstate, 0, %u);
+]], #lua_c_functions))
+for _, func in ipairs(lua_c_functions) do
+ output:write(string.format([[
+
+ lua_pushcfunction(lstate, &%s);
+ lua_setfield(lstate, -2, "%s");]], func.binding, func.api))
+end
+output:write([[
+
+ lua_setfield(lstate, -2, "api");
+}
+]])
+
+output:close()
diff --git a/src/clint.py b/src/clint.py
index 5174521fb8..3babb7772b 100755
--- a/src/clint.py
+++ b/src/clint.py
@@ -182,6 +182,7 @@ _ERROR_CATEGORIES = [
'build/include_order',
'build/printf_format',
'build/storage_class',
+ 'build/useless_fattr',
'readability/alt_tokens',
'readability/bool',
'readability/braces',
@@ -1225,6 +1226,10 @@ def CheckForHeaderGuard(filename, lines, error):
lines: An array of strings, each representing a line of the file.
error: The function to call with any errors found.
"""
+ if filename.endswith('.c.h') or FileInfo(filename).RelativePath() in set((
+ 'func_attr.h',
+ )):
+ return
cppvar = GetHeaderGuardCPPVariable(filename)
diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt
index f69781a539..8deeff4654 100644
--- a/src/nvim/CMakeLists.txt
+++ b/src/nvim/CMakeLists.txt
@@ -12,11 +12,10 @@ endif()
set(TOUCHES_DIR ${PROJECT_BINARY_DIR}/touches)
set(GENERATED_DIR ${PROJECT_BINARY_DIR}/src/nvim/auto)
-set(DISPATCH_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/gendispatch.lua)
-file(GLOB API_HEADERS api/*.h)
-file(GLOB MSGPACK_RPC_HEADERS msgpack_rpc/*.h)
+set(MSGPACK_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/genmsgpack.lua)
set(API_METADATA ${PROJECT_BINARY_DIR}/api_metadata.mpack)
set(FUNCS_DATA ${PROJECT_BINARY_DIR}/funcs_data.mpack)
+set(MSGPACK_LUA_C_BINDINGS ${GENERATED_DIR}/msgpack_lua_c_bindings.generated.c)
set(HEADER_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/gendeclarations.lua)
set(GENERATED_INCLUDES_DIR ${PROJECT_BINARY_DIR}/include)
set(GENERATED_API_DISPATCH ${GENERATED_DIR}/api/private/dispatch_wrappers.generated.h)
@@ -33,8 +32,10 @@ set(EVENTS_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/gen_events.lua)
set(OPTIONS_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/genoptions.lua)
set(UNICODE_TABLES_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/genunicodetables.lua)
set(UNICODE_DIR ${PROJECT_SOURCE_DIR}/unicode)
-file(GLOB UNICODE_FILES ${UNICODE_DIR}/*.txt)
set(GENERATED_UNICODE_TABLES ${GENERATED_DIR}/unicode_tables.generated.h)
+set(VIM_MODULE_FILE ${GENERATED_DIR}/lua/vim_module.generated.h)
+set(VIM_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/src/nvim/lua/vim.lua)
+set(CHAR_BLOB_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/gencharblob.lua)
set(LINT_SUPPRESS_FILE ${PROJECT_BINARY_DIR}/errors.json)
set(LINT_SUPPRESS_URL_BASE "https://raw.githubusercontent.com/neovim/doc/gh-pages/reports/clint")
set(LINT_SUPPRESS_URL "${LINT_SUPPRESS_URL_BASE}/errors.json")
@@ -46,6 +47,10 @@ set(LINT_SUPPRESSES_ARCHIVE ${LINT_SUPPRESSES_ROOT}/errors.tar.gz)
set(LINT_SUPPRESSES_TOUCH_FILE "${TOUCHES_DIR}/unpacked-clint-errors-archive")
set(LINT_SUPPRESSES_INSTALL_SCRIPT "${PROJECT_SOURCE_DIR}/cmake/InstallClintErrors.cmake")
+file(GLOB UNICODE_FILES ${UNICODE_DIR}/*.txt)
+file(GLOB API_HEADERS api/*.h)
+file(GLOB MSGPACK_RPC_HEADERS msgpack_rpc/*.h)
+
include_directories(${GENERATED_DIR})
include_directories(${CACHED_GENERATED_DIR})
include_directories(${GENERATED_INCLUDES_DIR})
@@ -67,6 +72,7 @@ foreach(subdir
tui
event
eval
+ lua
)
if(${subdir} MATCHES "tui" AND NOT FEAT_TUI)
continue()
@@ -157,7 +163,7 @@ if(CLANG_ASAN_UBSAN OR CLANG_MSAN OR CLANG_TSAN)
endif()
get_directory_property(gen_includes INCLUDE_DIRECTORIES)
-foreach(gen_include ${gen_includes})
+foreach(gen_include ${gen_includes} ${LUAJIT_INCLUDE_DIRS})
list(APPEND gen_cflags "-I${gen_include}")
endforeach()
string(TOUPPER "${CMAKE_BUILD_TYPE}" build_type)
@@ -219,18 +225,34 @@ add_custom_command(OUTPUT ${GENERATED_UNICODE_TABLES}
${UNICODE_FILES}
)
-add_custom_command(OUTPUT ${GENERATED_API_DISPATCH} ${GENERATED_FUNCS_METADATA}
- ${API_METADATA}
- COMMAND ${LUA_PRG} ${DISPATCH_GENERATOR} ${CMAKE_CURRENT_LIST_DIR}
- ${API_HEADERS} ${GENERATED_API_DISPATCH}
+add_custom_command(
+ OUTPUT ${GENERATED_API_DISPATCH} ${GENERATED_FUNCS_METADATA}
+ ${API_METADATA} ${MSGPACK_LUA_C_BINDINGS}
+ COMMAND ${LUA_PRG} ${MSGPACK_GENERATOR} ${CMAKE_CURRENT_LIST_DIR}
+ ${GENERATED_API_DISPATCH}
${GENERATED_FUNCS_METADATA} ${API_METADATA}
+ ${MSGPACK_LUA_C_BINDINGS}
+ ${API_HEADERS}
DEPENDS
${API_HEADERS}
${MSGPACK_RPC_HEADERS}
- ${DISPATCH_GENERATOR}
+ ${MSGPACK_GENERATOR}
${CMAKE_CURRENT_LIST_DIR}/api/dispatch_deprecated.lua
)
+add_custom_command(
+ OUTPUT ${VIM_MODULE_FILE}
+ COMMAND ${LUA_PRG} ${CHAR_BLOB_GENERATOR} ${VIM_MODULE_SOURCE}
+ ${VIM_MODULE_FILE} vim_module
+ DEPENDS
+ ${CHAR_BLOB_GENERATOR}
+ ${VIM_MODULE_SOURCE}
+)
+
+list(APPEND NVIM_GENERATED_SOURCES
+ "${MSGPACK_LUA_C_BINDINGS}"
+)
+
list(APPEND NVIM_GENERATED_FOR_HEADERS
"${GENERATED_EX_CMDS_ENUM}"
"${GENERATED_EVENTS_ENUM}"
@@ -242,6 +264,7 @@ list(APPEND NVIM_GENERATED_FOR_SOURCES
"${GENERATED_EVENTS_NAMES_MAP}"
"${GENERATED_OPTIONS}"
"${GENERATED_UNICODE_TABLES}"
+ "${VIM_MODULE_FILE}"
)
list(APPEND NVIM_GENERATED_SOURCES
@@ -315,6 +338,23 @@ if(UNIX)
endif()
set(NVIM_EXEC_LINK_LIBRARIES ${NVIM_LINK_LIBRARIES})
+set(NVIM_TEST_LINK_LIBRARIES ${NVIM_LINK_LIBRARIES})
+
+if(CMAKE_VERSION VERSION_LESS "2.8.8")
+ if(PREFER_LUAJIT)
+ include_directories(${LUAJIT_INCLUDE_DIRS})
+ else()
+ message(FATAL_ERROR
+ "Must support INCLUDE_DIRECTORIES target property to build")
+ endif()
+endif()
+
+if(PREFER_LUAJIT)
+ list(APPEND NVIM_EXEC_LINK_LIBRARIES ${LUAJIT_LIBRARIES})
+else()
+ list(APPEND NVIM_EXEC_LINK_LIBRARIES ${LUA_LIBRARIES})
+endif()
+list(APPEND NVIM_TEST_LINK_LIBRARIES ${LUAJIT_LIBRARIES})
# Don't use jemalloc in the unit test library.
if(JEMALLOC_FOUND)
@@ -326,6 +366,14 @@ add_executable(nvim ${NVIM_GENERATED_FOR_SOURCES} ${NVIM_GENERATED_FOR_HEADERS}
target_link_libraries(nvim ${NVIM_EXEC_LINK_LIBRARIES})
install_helper(TARGETS nvim)
+if(PREFER_LUAJIT)
+ set(LUA_PREFERRED_INCLUDE_DIRS ${LUAJIT_INCLUDE_DIRS})
+else()
+ set(LUA_PREFERRED_INCLUDE_DIRS ${LUA_INCLUDE_DIRS})
+endif()
+set_property(TARGET nvim APPEND PROPERTY
+ INCLUDE_DIRECTORIES ${LUA_PREFERRED_INCLUDE_DIRS})
+
if(WIN32)
# Copy DLLs and third-party tools to bin/ and install them along with nvim
add_custom_target(nvim_runtime_deps ALL
@@ -372,6 +420,50 @@ if(WIN32)
add_dependencies(nvim_runtime_deps external_blobs)
endif()
+add_library(
+ libnvim
+ STATIC
+ EXCLUDE_FROM_ALL
+ ${NVIM_SOURCES} ${NVIM_GENERATED_SOURCES}
+ ${NVIM_HEADERS} ${NVIM_GENERATED_FOR_SOURCES} ${NVIM_GENERATED_FOR_HEADERS}
+)
+set_property(TARGET libnvim APPEND PROPERTY
+ INCLUDE_DIRECTORIES ${LUAJIT_INCLUDE_DIRS})
+target_link_libraries(libnvim ${NVIM_TEST_LINK_LIBRARIES})
+set_target_properties(
+ libnvim
+ PROPERTIES
+ POSITION_INDEPENDENT_CODE ON
+ OUTPUT_NAME nvim
+)
+set_property(
+ TARGET libnvim
+ APPEND_STRING PROPERTY COMPILE_FLAGS " -DMAKE_LIB "
+)
+
+add_library(
+ nvim-test
+ MODULE
+ EXCLUDE_FROM_ALL
+ ${NVIM_SOURCES} ${NVIM_GENERATED_SOURCES}
+ ${NVIM_HEADERS} ${NVIM_GENERATED_FOR_SOURCES} ${NVIM_GENERATED_FOR_HEADERS}
+ ${UNIT_TEST_FIXTURES}
+)
+target_link_libraries(nvim-test ${NVIM_TEST_LINK_LIBRARIES})
+set_property(
+ TARGET nvim-test
+ APPEND PROPERTY INCLUDE_DIRECTORIES ${LUAJIT_INCLUDE_DIRS}
+)
+set_target_properties(
+ nvim-test
+ PROPERTIES
+ POSITION_INDEPENDENT_CODE ON
+)
+set_property(
+ TARGET nvim-test
+ APPEND_STRING PROPERTY COMPILE_FLAGS " -DUNIT_TESTING "
+)
+
if(CLANG_ASAN_UBSAN)
message(STATUS "Enabling Clang address sanitizer and undefined behavior sanitizer for nvim.")
check_c_compiler_flag(-fno-sanitize-recover=all SANITIZE_RECOVER_ALL)
@@ -396,19 +488,6 @@ elseif(CLANG_TSAN)
set_property(TARGET nvim APPEND_STRING PROPERTY LINK_FLAGS "-fsanitize=thread ")
endif()
-add_library(libnvim STATIC EXCLUDE_FROM_ALL ${NVIM_GENERATED_FOR_SOURCES} ${NVIM_GENERATED_FOR_HEADERS}
- ${NVIM_GENERATED_SOURCES} ${NVIM_SOURCES} ${NVIM_HEADERS})
-target_link_libraries(libnvim ${NVIM_LINK_LIBRARIES})
-set_target_properties(libnvim PROPERTIES
- POSITION_INDEPENDENT_CODE ON
- OUTPUT_NAME nvim)
-set_property(TARGET libnvim APPEND_STRING PROPERTY COMPILE_FLAGS " -DMAKE_LIB ")
-
-add_library(nvim-test MODULE EXCLUDE_FROM_ALL ${NVIM_GENERATED_FOR_SOURCES} ${NVIM_GENERATED_FOR_HEADERS}
- ${NVIM_GENERATED_SOURCES} ${NVIM_SOURCES} ${UNIT_TEST_FIXTURES} ${NVIM_HEADERS})
-target_link_libraries(nvim-test ${NVIM_LINK_LIBRARIES})
-set_property(TARGET nvim-test APPEND_STRING PROPERTY COMPILE_FLAGS -DUNIT_TESTING)
-
function(get_test_target prefix sfile relative_path_var target_var)
get_filename_component(full_d "${sfile}" PATH)
file(RELATIVE_PATH d "${PROJECT_SOURCE_DIR}/src/nvim" "${full_d}")
@@ -446,6 +525,10 @@ foreach(hfile ${NVIM_HEADERS})
${texe}
EXCLUDE_FROM_ALL
${tsource} ${NVIM_HEADERS} ${NVIM_GENERATED_FOR_HEADERS})
+ set_property(
+ TARGET ${texe}
+ APPEND PROPERTY INCLUDE_DIRECTORIES ${LUA_PREFERRED_INCLUDE_DIRS}
+ )
list(FIND NO_SINGLE_CHECK_HEADERS "${relative_path}" hfile_exclude_idx)
if(${hfile_exclude_idx} EQUAL -1)
diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c
index 5de1535bce..0b8d39d0d2 100644
--- a/src/nvim/api/buffer.c
+++ b/src/nvim/api/buffer.c
@@ -195,7 +195,7 @@ ArrayOf(String) nvim_buf_get_lines(uint64_t channel_id,
Object str = STRING_OBJ(cstr_to_string(bufstr));
// Vim represents NULs as NLs, but this may confuse clients.
- if (channel_id != INTERNAL_CALL) {
+ if (channel_id != VIML_INTERNAL_CALL) {
strchrsub(str.data.string.data, '\n', '\0');
}
@@ -295,6 +295,24 @@ void nvim_buf_set_lines(uint64_t channel_id,
return;
}
+ for (size_t i = 0; i < replacement.size; i++) {
+ if (replacement.items[i].type != kObjectTypeString) {
+ api_set_error(err,
+ kErrorTypeValidation,
+ "All items in the replacement array must be strings");
+ return;
+ }
+ // Disallow newlines in the middle of the line.
+ if (channel_id != VIML_INTERNAL_CALL) {
+ const String l = replacement.items[i].data.string;
+ if (memchr(l.data, NL, l.size)) {
+ api_set_error(err, kErrorTypeValidation,
+ "String cannot contain newlines");
+ return;
+ }
+ }
+ }
+
win_T *save_curwin = NULL;
tabpage_T *save_curtab = NULL;
size_t new_len = replacement.size;
@@ -303,27 +321,12 @@ void nvim_buf_set_lines(uint64_t channel_id,
char **lines = (new_len != 0) ? xcalloc(new_len, sizeof(char *)) : NULL;
for (size_t i = 0; i < new_len; i++) {
- if (replacement.items[i].type != kObjectTypeString) {
- api_set_error(err,
- kErrorTypeValidation,
- "All items in the replacement array must be strings");
- goto end;
- }
-
- String l = replacement.items[i].data.string;
+ const String l = replacement.items[i].data.string;
- // Fill lines[i] with l's contents. Disallow newlines in the middle of a
- // line and convert NULs to newlines to avoid truncation.
- lines[i] = xmallocz(l.size);
- for (size_t j = 0; j < l.size; j++) {
- if (l.data[j] == '\n' && channel_id != INTERNAL_CALL) {
- api_set_error(err, kErrorTypeException,
- "String cannot contain newlines");
- new_len = i + 1;
- goto end;
- }
- lines[i][j] = (char) (l.data[j] == '\0' ? '\n' : l.data[j]);
- }
+ // Fill lines[i] with l's contents. Convert NULs to newlines as required by
+ // NL-used-for-NUL.
+ lines[i] = xmemdupz(l.data, l.size);
+ memchrsub(lines[i], NUL, NL, l.size);
}
try_start();
diff --git a/src/nvim/api/private/defs.h b/src/nvim/api/private/defs.h
index 60bf38265f..2144c80d6a 100644
--- a/src/nvim/api/private/defs.h
+++ b/src/nvim/api/private/defs.h
@@ -5,6 +5,8 @@
#include <stdbool.h>
#include <string.h>
+#include "nvim/func_attr.h"
+
#define ARRAY_DICT_INIT {.size = 0, .capacity = 0, .items = NULL}
#define STRING_INIT {.data = NULL, .size = 0}
#define OBJECT_INIT { .type = kObjectTypeNil }
@@ -36,8 +38,27 @@ typedef enum {
/// Used as the message ID of notifications.
#define NO_RESPONSE UINT64_MAX
-/// Used as channel_id when the call is local.
-#define INTERNAL_CALL UINT64_MAX
+/// Mask for all internal calls
+#define INTERNAL_CALL_MASK (((uint64_t)1) << (sizeof(uint64_t) * 8 - 1))
+
+/// Internal call from VimL code
+#define VIML_INTERNAL_CALL INTERNAL_CALL_MASK
+
+/// Internal call from lua code
+#define LUA_INTERNAL_CALL (VIML_INTERNAL_CALL + 1)
+
+static inline bool is_internal_call(uint64_t channel_id)
+ REAL_FATTR_ALWAYS_INLINE REAL_FATTR_CONST;
+
+/// Check whether call is internal
+///
+/// @param[in] channel_id Channel id.
+///
+/// @return true if channel_id refers to internal channel.
+static inline bool is_internal_call(const uint64_t channel_id)
+{
+ return !!(channel_id & INTERNAL_CALL_MASK);
+}
typedef struct {
ErrorType type;
@@ -78,16 +99,17 @@ typedef struct {
} Dictionary;
typedef enum {
- kObjectTypeBuffer,
- kObjectTypeWindow,
- kObjectTypeTabpage,
- kObjectTypeNil,
+ kObjectTypeNil = 0,
kObjectTypeBoolean,
kObjectTypeInteger,
kObjectTypeFloat,
kObjectTypeString,
kObjectTypeArray,
kObjectTypeDictionary,
+ // EXT types, cannot be split or reordered, see #EXT_OBJECT_TYPE_SHIFT
+ kObjectTypeBuffer,
+ kObjectTypeWindow,
+ kObjectTypeTabpage,
} ObjectType;
struct object {
diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c
index 69cb19c14f..4cc4b600d5 100644
--- a/src/nvim/api/private/helpers.c
+++ b/src/nvim/api/private/helpers.c
@@ -353,7 +353,7 @@ void set_option_to(void *to, int type, String name, Object value, Error *err)
#define TYPVAL_ENCODE_CONV_UNSIGNED_NUMBER TYPVAL_ENCODE_CONV_NUMBER
#define TYPVAL_ENCODE_CONV_FLOAT(tv, flt) \
- kv_push(edata->stack, FLOATING_OBJ((Float)(flt)))
+ kv_push(edata->stack, FLOAT_OBJ((Float)(flt)))
#define TYPVAL_ENCODE_CONV_STRING(tv, str, len) \
do { \
@@ -864,15 +864,18 @@ static void init_type_metadata(Dictionary *metadata)
Dictionary types = ARRAY_DICT_INIT;
Dictionary buffer_metadata = ARRAY_DICT_INIT;
- PUT(buffer_metadata, "id", INTEGER_OBJ(kObjectTypeBuffer));
+ PUT(buffer_metadata, "id",
+ INTEGER_OBJ(kObjectTypeBuffer - EXT_OBJECT_TYPE_SHIFT));
PUT(buffer_metadata, "prefix", STRING_OBJ(cstr_to_string("nvim_buf_")));
Dictionary window_metadata = ARRAY_DICT_INIT;
- PUT(window_metadata, "id", INTEGER_OBJ(kObjectTypeWindow));
+ PUT(window_metadata, "id",
+ INTEGER_OBJ(kObjectTypeWindow - EXT_OBJECT_TYPE_SHIFT));
PUT(window_metadata, "prefix", STRING_OBJ(cstr_to_string("nvim_win_")));
Dictionary tabpage_metadata = ARRAY_DICT_INIT;
- PUT(tabpage_metadata, "id", INTEGER_OBJ(kObjectTypeTabpage));
+ PUT(tabpage_metadata, "id",
+ INTEGER_OBJ(kObjectTypeTabpage - EXT_OBJECT_TYPE_SHIFT));
PUT(tabpage_metadata, "prefix", STRING_OBJ(cstr_to_string("nvim_tabpage_")));
PUT(types, "Buffer", DICTIONARY_OBJ(buffer_metadata));
diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h
index 681945ac9c..159b9d5c2a 100644
--- a/src/nvim/api/private/helpers.h
+++ b/src/nvim/api/private/helpers.h
@@ -18,7 +18,7 @@
.type = kObjectTypeInteger, \
.data.integer = i })
-#define FLOATING_OBJ(f) ((Object) { \
+#define FLOAT_OBJ(f) ((Object) { \
.type = kObjectTypeFloat, \
.data.floating = f })
diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c
index 11f15b5ad1..12e8194c15 100644
--- a/src/nvim/api/vim.c
+++ b/src/nvim/api/vim.c
@@ -144,7 +144,7 @@ String nvim_replace_termcodes(String str, Boolean from_part, Boolean do_lt,
{
if (str.size == 0) {
// Empty string
- return str;
+ return (String) { .data = NULL, .size = 0 };
}
char *ptr = NULL;
@@ -203,7 +203,8 @@ Object nvim_eval(String expr, Error *err)
return rv;
}
-/// Calls a VimL function with the given arguments.
+/// Calls a VimL function with the given arguments
+///
/// On VimL error: Returns a generic error; v:errmsg is not updated.
///
/// @param fname Function to call
@@ -855,3 +856,57 @@ static void write_msg(String message, bool to_err)
--no_wait_return;
msg_end();
}
+
+// Functions used for testing purposes
+
+/// Returns object given as argument
+///
+/// This API function is used for testing. One should not rely on its presence
+/// in plugins.
+///
+/// @param[in] obj Object to return.
+///
+/// @return its argument.
+Object nvim__id(Object obj)
+{
+ return copy_object(obj);
+}
+
+/// Returns array given as argument
+///
+/// This API function is used for testing. One should not rely on its presence
+/// in plugins.
+///
+/// @param[in] arr Array to return.
+///
+/// @return its argument.
+Array nvim__id_array(Array arr)
+{
+ return copy_object(ARRAY_OBJ(arr)).data.array;
+}
+
+/// Returns dictionary given as argument
+///
+/// This API function is used for testing. One should not rely on its presence
+/// in plugins.
+///
+/// @param[in] dct Dictionary to return.
+///
+/// @return its argument.
+Dictionary nvim__id_dictionary(Dictionary dct)
+{
+ return copy_object(DICTIONARY_OBJ(dct)).data.dictionary;
+}
+
+/// Returns floating-point value given as argument
+///
+/// This API function is used for testing. One should not rely on its presence
+/// in plugins.
+///
+/// @param[in] flt Value to return.
+///
+/// @return its argument.
+Float nvim__id_float(Float flt)
+{
+ return flt;
+}
diff --git a/src/nvim/api/window.c b/src/nvim/api/window.c
index 859bf88398..22902800ea 100644
--- a/src/nvim/api/window.c
+++ b/src/nvim/api/window.c
@@ -62,6 +62,10 @@ void nvim_win_set_cursor(Window window, ArrayOf(Integer, 2) pos, Error *err)
{
win_T *win = find_window_by_handle(window, err);
+ if (!win) {
+ return;
+ }
+
if (pos.size != 2 || pos.items[0].type != kObjectTypeInteger
|| pos.items[1].type != kObjectTypeInteger) {
api_set_error(err,
@@ -70,10 +74,6 @@ void nvim_win_set_cursor(Window window, ArrayOf(Integer, 2) pos, Error *err)
return;
}
- if (!win) {
- return;
- }
-
int64_t row = pos.items[0].data.integer;
int64_t col = pos.items[1].data.integer;
diff --git a/src/nvim/eval.c b/src/nvim/eval.c
index 1bf85ccd8b..56a6632fad 100644
--- a/src/nvim/eval.c
+++ b/src/nvim/eval.c
@@ -97,6 +97,7 @@
#include "nvim/lib/kvec.h"
#include "nvim/lib/khash.h"
#include "nvim/lib/queue.h"
+#include "nvim/lua/executor.h"
#include "nvim/eval/typval.h"
#include "nvim/eval/executor.h"
#include "nvim/eval/gc.h"
@@ -6510,7 +6511,7 @@ static void api_wrapper(typval_T *argvars, typval_T *rettv, FunPtr fptr)
}
Error err = ERROR_INIT;
- Object result = fn(INTERNAL_CALL, args, &err);
+ Object result = fn(VIML_INTERNAL_CALL, args, &err);
if (ERROR_SET(&err)) {
nvim_err_writeln(cstr_as_string(err.msg));
@@ -10973,7 +10974,9 @@ static int inputsecret_flag = 0;
* prompt. The third argument to f_inputdialog() specifies the value to return
* when the user cancels the prompt.
*/
-static void get_user_input(typval_T *argvars, typval_T *rettv, int inputdialog)
+void get_user_input(const typval_T *const argvars,
+ typval_T *const rettv, const bool inputdialog)
+ FUNC_ATTR_NONNULL_ALL
{
const char *prompt = tv_get_string_chk(&argvars[0]);
int cmd_silent_save = cmd_silent;
@@ -12076,6 +12079,18 @@ static void get_maparg(typval_T *argvars, typval_T *rettv, int exact)
}
}
+/// luaeval() function implementation
+static void f_luaeval(typval_T *argvars, typval_T *rettv, FunPtr fptr)
+ FUNC_ATTR_NONNULL_ALL
+{
+ const char *const str = (const char *)tv_get_string_chk(&argvars[0]);
+ if (str == NULL) {
+ return;
+ }
+
+ executor_eval_lua(cstr_as_string((char *)str), &argvars[1], rettv);
+}
+
/*
* "map()" function
*/
diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua
index 21b5bbb139..533403b2b0 100644
--- a/src/nvim/eval.lua
+++ b/src/nvim/eval.lua
@@ -193,6 +193,7 @@ return {
localtime={},
log={args=1, func="float_op_wrapper", data="&log"},
log10={args=1, func="float_op_wrapper", data="&log10"},
+ luaeval={args={1, 2}},
map={args=2},
maparg={args={1, 4}},
mapcheck={args={1, 3}},
diff --git a/src/nvim/eval/decode.c b/src/nvim/eval/decode.c
index 8905317f15..935c98fb84 100644
--- a/src/nvim/eval/decode.c
+++ b/src/nvim/eval/decode.c
@@ -11,6 +11,7 @@
#include "nvim/ascii.h"
#include "nvim/macros.h"
#include "nvim/message.h"
+#include "nvim/globals.h"
#include "nvim/charset.h" // vim_str2nr
#include "nvim/lib/kvec.h"
#include "nvim/vim.h" // OK, FAIL
@@ -223,6 +224,78 @@ static inline int json_decoder_pop(ValuesStackItem obj,
} \
} while (0)
+/// Create a new special dictionary that ought to represent a MAP
+///
+/// @param[out] ret_tv Address where new special dictionary is saved.
+///
+/// @return [allocated] list which should contain key-value pairs. Return value
+/// may be safely ignored.
+list_T *decode_create_map_special_dict(typval_T *const ret_tv)
+ FUNC_ATTR_NONNULL_ALL
+{
+ list_T *const list = tv_list_alloc();
+ list->lv_refcount++;
+ create_special_dict(ret_tv, kMPMap, ((typval_T) {
+ .v_type = VAR_LIST,
+ .v_lock = VAR_UNLOCKED,
+ .vval = { .v_list = list },
+ }));
+ return list;
+}
+
+/// Convert char* string to typval_T
+///
+/// Depending on whether string has (no) NUL bytes, it may use a special
+/// dictionary or decode string to VAR_STRING.
+///
+/// @param[in] s String to decode.
+/// @param[in] len String length.
+/// @param[in] hasnul Whether string has NUL byte, not or it was not yet
+/// determined.
+/// @param[in] binary If true, save special string type as kMPBinary,
+/// otherwise kMPString.
+/// @param[in] s_allocated If true, then `s` was allocated and can be saved in
+/// a returned structure. If it is not saved there, it
+/// will be freed.
+///
+/// @return Decoded string.
+typval_T decode_string(const char *const s, const size_t len,
+ const TriState hasnul, const bool binary,
+ const bool s_allocated)
+ FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ assert(s != NULL || len == 0);
+ const bool really_hasnul = (hasnul == kNone
+ ? memchr(s, NUL, len) != NULL
+ : (bool)hasnul);
+ if (really_hasnul) {
+ list_T *const list = tv_list_alloc();
+ list->lv_refcount++;
+ typval_T tv;
+ create_special_dict(&tv, binary ? kMPBinary : kMPString, ((typval_T) {
+ .v_type = VAR_LIST,
+ .v_lock = VAR_UNLOCKED,
+ .vval = { .v_list = list },
+ }));
+ const int elw_ret = encode_list_write((void *)list, s, len);
+ if (s_allocated) {
+ xfree((void *)s);
+ }
+ if (elw_ret == -1) {
+ tv_clear(&tv);
+ return (typval_T) { .v_type = VAR_UNKNOWN, .v_lock = VAR_UNLOCKED };
+ }
+ return tv;
+ } else {
+ return (typval_T) {
+ .v_type = VAR_STRING,
+ .v_lock = VAR_UNLOCKED,
+ .vval = { .v_string = (char_u *)(
+ s_allocated ? (char *)s : xmemdupz(s, len)) },
+ };
+ }
+}
+
/// Parse JSON double-quoted string
///
/// @param[in] buf Buffer being converted.
@@ -416,29 +489,13 @@ static inline int parse_json_string(const char *const buf, const size_t buf_len,
}
PUT_FST_IN_PAIR(fst_in_pair, str_end);
#undef PUT_FST_IN_PAIR
- if (hasnul) {
- typval_T obj;
- list_T *const list = tv_list_alloc();
- list->lv_refcount++;
- create_special_dict(&obj, kMPString, ((typval_T) {
- .v_type = VAR_LIST,
- .v_lock = VAR_UNLOCKED,
- .vval = { .v_list = list },
- }));
- if (encode_list_write((void *) list, str, (size_t) (str_end - str))
- == -1) {
- tv_clear(&obj);
- goto parse_json_string_fail;
- }
- xfree(str);
- POP(obj, true);
- } else {
- *str_end = NUL;
- POP(((typval_T) {
- .v_type = VAR_STRING,
- .vval = { .v_string = (char_u *) str },
- }), false);
+ *str_end = NUL;
+ typval_T obj = decode_string(
+ str, (size_t)(str_end - str), hasnul ? kTrue : kFalse, false, true);
+ if (obj.v_type == VAR_UNKNOWN) {
+ goto parse_json_string_fail;
}
+ POP(obj, obj.v_type != VAR_STRING);
goto parse_json_string_ret;
parse_json_string_fail:
ret = FAIL;
@@ -812,13 +869,7 @@ json_decode_string_cycle_start:
list_T *val_list = NULL;
if (next_map_special) {
next_map_special = false;
- val_list = tv_list_alloc();
- val_list->lv_refcount++;
- create_special_dict(&tv, kMPMap, ((typval_T) {
- .v_type = VAR_LIST,
- .v_lock = VAR_UNLOCKED,
- .vval = { .v_list = val_list },
- }));
+ val_list = decode_create_map_special_dict(&tv);
} else {
dict_T *dict = tv_dict_alloc();
dict->dv_refcount++;
@@ -971,37 +1022,17 @@ int msgpack_to_vim(const msgpack_object mobj, typval_T *const rettv)
break;
}
case MSGPACK_OBJECT_STR: {
- list_T *const list = tv_list_alloc();
- list->lv_refcount++;
- create_special_dict(rettv, kMPString, ((typval_T) {
- .v_type = VAR_LIST,
- .v_lock = VAR_UNLOCKED,
- .vval = { .v_list = list },
- }));
- if (encode_list_write((void *) list, mobj.via.str.ptr, mobj.via.str.size)
- == -1) {
+ *rettv = decode_string(mobj.via.bin.ptr, mobj.via.bin.size, kTrue, false,
+ false);
+ if (rettv->v_type == VAR_UNKNOWN) {
return FAIL;
}
break;
}
case MSGPACK_OBJECT_BIN: {
- if (memchr(mobj.via.bin.ptr, NUL, mobj.via.bin.size) == NULL) {
- *rettv = (typval_T) {
- .v_type = VAR_STRING,
- .v_lock = VAR_UNLOCKED,
- .vval = { .v_string = xmemdupz(mobj.via.bin.ptr, mobj.via.bin.size) },
- };
- break;
- }
- list_T *const list = tv_list_alloc();
- list->lv_refcount++;
- create_special_dict(rettv, kMPBinary, ((typval_T) {
- .v_type = VAR_LIST,
- .v_lock = VAR_UNLOCKED,
- .vval = { .v_list = list },
- }));
- if (encode_list_write((void *) list, mobj.via.bin.ptr, mobj.via.bin.size)
- == -1) {
+ *rettv = decode_string(mobj.via.bin.ptr, mobj.via.bin.size, kNone, true,
+ false);
+ if (rettv->v_type == VAR_UNKNOWN) {
return FAIL;
}
break;
@@ -1058,13 +1089,7 @@ int msgpack_to_vim(const msgpack_object mobj, typval_T *const rettv)
}
break;
msgpack_to_vim_generic_map: {}
- list_T *const list = tv_list_alloc();
- list->lv_refcount++;
- create_special_dict(rettv, kMPMap, ((typval_T) {
- .v_type = VAR_LIST,
- .v_lock = VAR_UNLOCKED,
- .vval = { .v_list = list },
- }));
+ list_T *const list = decode_create_map_special_dict(rettv);
for (size_t i = 0; i < mobj.via.map.size; i++) {
list_T *const kv_pair = tv_list_alloc();
tv_list_append_list(list, kv_pair);
diff --git a/src/nvim/eval/decode.h b/src/nvim/eval/decode.h
index c8e7a189e3..77fc4c78c2 100644
--- a/src/nvim/eval/decode.h
+++ b/src/nvim/eval/decode.h
@@ -6,6 +6,7 @@
#include <msgpack.h>
#include "nvim/eval/typval.h"
+#include "nvim/globals.h"
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "eval/decode.h.generated.h"
diff --git a/src/nvim/eval/typval_encode.c.h b/src/nvim/eval/typval_encode.c.h
index b4a70fb188..a93ad2dbba 100644
--- a/src/nvim/eval/typval_encode.c.h
+++ b/src/nvim/eval/typval_encode.c.h
@@ -233,10 +233,6 @@
///
/// This name will only be used by one of the above macros which are defined by
/// the caller. Functions defined here do not use first argument directly.
-#ifndef NVIM_EVAL_TYPVAL_ENCODE_C_H
-#define NVIM_EVAL_TYPVAL_ENCODE_C_H
-#undef NVIM_EVAL_TYPVAL_ENCODE_C_H
-
#include <stddef.h>
#include <inttypes.h>
#include <assert.h>
@@ -816,4 +812,3 @@ encode_vim_to__error_ret:
// Prevent “unused label” warnings.
goto typval_encode_stop_converting_one_item; // -V779
}
-#endif // NVIM_EVAL_TYPVAL_ENCODE_C_H
diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua
index 523740444d..7203fbd97d 100644
--- a/src/nvim/ex_cmds.lua
+++ b/src/nvim/ex_cmds.lua
@@ -1548,19 +1548,19 @@ return {
command='lua',
flags=bit.bor(RANGE, EXTRA, NEEDARG, CMDWIN),
addr_type=ADDR_LINES,
- func='ex_script_ni',
+ func='ex_lua',
},
{
command='luado',
flags=bit.bor(RANGE, DFLALL, EXTRA, NEEDARG, CMDWIN),
addr_type=ADDR_LINES,
- func='ex_ni',
+ func='ex_luado',
},
{
command='luafile',
flags=bit.bor(RANGE, FILE1, NEEDARG, CMDWIN),
addr_type=ADDR_LINES,
- func='ex_ni',
+ func='ex_luafile',
},
{
command='lvimgrep',
diff --git a/src/nvim/ex_cmds2.c b/src/nvim/ex_cmds2.c
index a1a32d9f52..1a728647ca 100644
--- a/src/nvim/ex_cmds2.c
+++ b/src/nvim/ex_cmds2.c
@@ -3705,19 +3705,18 @@ char_u *get_locales(expand_T *xp, int idx)
static void script_host_execute(char *name, exarg_T *eap)
{
- uint8_t *script = script_get(eap, eap->arg);
+ size_t len;
+ char *const script = script_get(eap, &len);
- if (!eap->skip) {
- list_T *args = tv_list_alloc();
+ if (script != NULL) {
+ list_T *const args = tv_list_alloc();
// script
- tv_list_append_string(args, (const char *)(script ? script : eap->arg), -1);
+ tv_list_append_allocated_string(args, script);
// current range
tv_list_append_number(args, (int)eap->line1);
tv_list_append_number(args, (int)eap->line2);
(void)eval_call_provider(name, "execute", args);
}
-
- xfree(script);
}
static void script_host_execute_file(char *name, exarg_T *eap)
diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c
index 108b7483b9..cb02befeaf 100644
--- a/src/nvim/ex_docmd.c
+++ b/src/nvim/ex_docmd.c
@@ -70,6 +70,7 @@
#include "nvim/event/rstream.h"
#include "nvim/event/wstream.h"
#include "nvim/shada.h"
+#include "nvim/lua/executor.h"
#include "nvim/globals.h"
static int quitmore = 0;
@@ -3807,10 +3808,12 @@ void ex_ni(exarg_T *eap)
/// Skips over ":perl <<EOF" constructs.
static void ex_script_ni(exarg_T *eap)
{
- if (!eap->skip)
+ if (!eap->skip) {
ex_ni(eap);
- else
- xfree(script_get(eap, eap->arg));
+ } else {
+ size_t len;
+ xfree(script_get(eap, &len));
+ }
}
/*
@@ -5816,10 +5819,10 @@ int parse_addr_type_arg(char_u *value, int vallen, uint32_t *argt,
* copied to allocated memory and stored in "*compl_arg".
* Returns FAIL if something is wrong.
*/
-int parse_compl_arg(char_u *value, int vallen, int *complp,
+int parse_compl_arg(const char_u *value, int vallen, int *complp,
uint32_t *argt, char_u **compl_arg)
{
- char_u *arg = NULL;
+ const char_u *arg = NULL;
size_t arglen = 0;
int i;
int valend = vallen;
diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c
index ff68de7670..fe45ba4568 100644
--- a/src/nvim/ex_getln.c
+++ b/src/nvim/ex_getln.c
@@ -5380,47 +5380,61 @@ static int ex_window(void)
return cmdwin_result;
}
-/*
- * Used for commands that either take a simple command string argument, or:
- * cmd << endmarker
- * {script}
- * endmarker
- * Returns a pointer to allocated memory with {script} or NULL.
- */
-char_u *script_get(exarg_T *eap, char_u *cmd)
+/// Get script string
+///
+/// Used for commands which accept either `:command script` or
+///
+/// :command << endmarker
+/// script
+/// endmarker
+///
+/// @param eap Command being run.
+/// @param[out] lenp Location where length of resulting string is saved. Will
+/// be set to zero when skipping.
+///
+/// @return [allocated] NULL or script. Does not show any error messages.
+/// NULL is returned when skipping and on error.
+char *script_get(exarg_T *const eap, size_t *const lenp)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_MALLOC
{
- char_u *theline;
- char *end_pattern = NULL;
- char dot[] = ".";
- garray_T ga;
+ const char *const cmd = (const char *)eap->arg;
- if (cmd[0] != '<' || cmd[1] != '<' || eap->getline == NULL)
- return NULL;
-
- ga_init(&ga, 1, 0x400);
+ if (cmd[0] != '<' || cmd[1] != '<' || eap->getline == NULL) {
+ *lenp = STRLEN(eap->arg);
+ return xmemdupz(eap->arg, *lenp);
+ }
- if (cmd[2] != NUL)
- end_pattern = (char *)skipwhite(cmd + 2);
- else
- end_pattern = dot;
+ garray_T ga = { .ga_data = NULL, .ga_len = 0 };
+ if (!eap->skip) {
+ ga_init(&ga, 1, 0x400);
+ }
- for (;; ) {
- theline = eap->getline(
+ const char *const end_pattern = (
+ cmd[2] != NUL
+ ? (const char *)skipwhite((const char_u *)cmd + 2)
+ : ".");
+ for (;;) {
+ char *const theline = (char *)eap->getline(
eap->cstack->cs_looplevel > 0 ? -1 :
NUL, eap->cookie, 0);
- if (theline == NULL || STRCMP(end_pattern, theline) == 0) {
+ if (theline == NULL || strcmp(end_pattern, theline) == 0) {
xfree(theline);
break;
}
- ga_concat(&ga, theline);
- ga_append(&ga, '\n');
+ if (!eap->skip) {
+ ga_concat(&ga, (const char_u *)theline);
+ ga_append(&ga, '\n');
+ }
xfree(theline);
}
- ga_append(&ga, NUL);
+ *lenp = (size_t)ga.ga_len; // Set length without trailing NUL.
+ if (!eap->skip) {
+ ga_append(&ga, NUL);
+ }
- return (char_u *)ga.ga_data;
+ return (char *)ga.ga_data;
}
/// Iterate over history items
diff --git a/src/nvim/func_attr.h b/src/nvim/func_attr.h
index 756c6d05ee..1049b5b7a6 100644
--- a/src/nvim/func_attr.h
+++ b/src/nvim/func_attr.h
@@ -41,10 +41,6 @@
// $ gcc -E -dM - </dev/null
// $ echo | clang -dM -E -
-#ifndef NVIM_FUNC_ATTR_H
-#define NVIM_FUNC_ATTR_H
-#undef NVIM_FUNC_ATTR_H
-
#ifdef FUNC_ATTR_MALLOC
# undef FUNC_ATTR_MALLOC
#endif
@@ -214,4 +210,3 @@
# define FUNC_ATTR_NONNULL_ARG(...)
# define FUNC_ATTR_NONNULL_RET
#endif
-#endif // NVIM_FUNC_ATTR_H
diff --git a/src/nvim/lua/converter.c b/src/nvim/lua/converter.c
new file mode 100644
index 0000000000..7a6167350f
--- /dev/null
+++ b/src/nvim/lua/converter.c
@@ -0,0 +1,1186 @@
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+#include <assert.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#include "nvim/api/private/defs.h"
+#include "nvim/api/private/helpers.h"
+#include "nvim/func_attr.h"
+#include "nvim/memory.h"
+#include "nvim/assert.h"
+// FIXME: vim.h is not actually needed, but otherwise it states MAXPATHL is
+// redefined
+#include "nvim/vim.h"
+#include "nvim/globals.h"
+#include "nvim/message.h"
+#include "nvim/eval/typval.h"
+#include "nvim/ascii.h"
+#include "nvim/macros.h"
+
+#include "nvim/lib/kvec.h"
+#include "nvim/eval/decode.h"
+
+#include "nvim/lua/converter.h"
+#include "nvim/lua/executor.h"
+
+/// Determine, which keys lua table contains
+typedef struct {
+ size_t maxidx; ///< Maximum positive integral value found.
+ size_t string_keys_num; ///< Number of string keys.
+ bool has_string_with_nul; ///< True if there is string key with NUL byte.
+ ObjectType type; ///< If has_type_key is true then attached value. Otherwise
+ ///< either kObjectTypeNil, kObjectTypeDictionary or
+ ///< kObjectTypeArray, depending on other properties.
+ lua_Number val; ///< If has_val_key and val_type == LUA_TNUMBER: value.
+ bool has_type_key; ///< True if type key is present.
+} LuaTableProps;
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "lua/converter.c.generated.h"
+#endif
+
+#define TYPE_IDX_VALUE true
+#define VAL_IDX_VALUE false
+
+#define LUA_PUSH_STATIC_STRING(lstate, s) \
+ lua_pushlstring(lstate, s, sizeof(s) - 1)
+
+static LuaTableProps nlua_traverse_table(lua_State *const lstate)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ size_t tsize = 0; // Total number of keys.
+ int val_type = 0; // If has_val_key: lua type of the value.
+ bool has_val_key = false; // True if val key was found,
+ // @see nlua_push_val_idx().
+ size_t other_keys_num = 0; // Number of keys that are not string, integral
+ // or type keys.
+ LuaTableProps ret;
+ memset(&ret, 0, sizeof(ret));
+ if (!lua_checkstack(lstate, lua_gettop(lstate) + 3)) {
+ emsgf(_("E1502: Lua failed to grow stack to %i"), lua_gettop(lstate) + 2);
+ ret.type = kObjectTypeNil;
+ return ret;
+ }
+ lua_pushnil(lstate);
+ while (lua_next(lstate, -2)) {
+ switch (lua_type(lstate, -2)) {
+ case LUA_TSTRING: {
+ size_t len;
+ const char *s = lua_tolstring(lstate, -2, &len);
+ if (memchr(s, NUL, len) != NULL) {
+ ret.has_string_with_nul = true;
+ }
+ ret.string_keys_num++;
+ break;
+ }
+ case LUA_TNUMBER: {
+ const lua_Number n = lua_tonumber(lstate, -2);
+ if (n > (lua_Number)SIZE_MAX || n <= 0
+ || ((lua_Number)((size_t)n)) != n) {
+ other_keys_num++;
+ } else {
+ const size_t idx = (size_t)n;
+ if (idx > ret.maxidx) {
+ ret.maxidx = idx;
+ }
+ }
+ break;
+ }
+ case LUA_TBOOLEAN: {
+ const bool b = lua_toboolean(lstate, -2);
+ if (b == TYPE_IDX_VALUE) {
+ if (lua_type(lstate, -1) == LUA_TNUMBER) {
+ lua_Number n = lua_tonumber(lstate, -1);
+ if (n == (lua_Number)kObjectTypeFloat
+ || n == (lua_Number)kObjectTypeArray
+ || n == (lua_Number)kObjectTypeDictionary) {
+ ret.has_type_key = true;
+ ret.type = (ObjectType)n;
+ } else {
+ other_keys_num++;
+ }
+ } else {
+ other_keys_num++;
+ }
+ } else {
+ has_val_key = true;
+ val_type = lua_type(lstate, -1);
+ if (val_type == LUA_TNUMBER) {
+ ret.val = lua_tonumber(lstate, -1);
+ }
+ }
+ break;
+ }
+ default: {
+ other_keys_num++;
+ break;
+ }
+ }
+ tsize++;
+ lua_pop(lstate, 1);
+ }
+ if (ret.has_type_key) {
+ if (ret.type == kObjectTypeFloat
+ && (!has_val_key || val_type != LUA_TNUMBER)) {
+ ret.type = kObjectTypeNil;
+ } else if (ret.type == kObjectTypeArray) {
+ // Determine what is the last number in a *sequence* of keys.
+ // This condition makes sure that Neovim will not crash when it gets table
+ // {[vim.type_idx]=vim.types.array, [SIZE_MAX]=1}: without it maxidx will
+ // be SIZE_MAX, with this condition it should be zero and [SIZE_MAX] key
+ // should be ignored.
+ if (ret.maxidx != 0
+ && ret.maxidx != (tsize
+ - ret.has_type_key
+ - other_keys_num
+ - has_val_key
+ - ret.string_keys_num)) {
+ for (ret.maxidx = 0;; ret.maxidx++) {
+ lua_rawgeti(lstate, -1, (int)ret.maxidx + 1);
+ if (lua_isnil(lstate, -1)) {
+ lua_pop(lstate, 1);
+ break;
+ }
+ lua_pop(lstate, 1);
+ }
+ }
+ }
+ } else {
+ if (tsize == 0
+ || (tsize == ret.maxidx
+ && other_keys_num == 0
+ && ret.string_keys_num == 0)) {
+ ret.type = kObjectTypeArray;
+ } else if (ret.string_keys_num == tsize) {
+ ret.type = kObjectTypeDictionary;
+ } else {
+ ret.type = kObjectTypeNil;
+ }
+ }
+ return ret;
+}
+
+/// Helper structure for nlua_pop_typval
+typedef struct {
+ typval_T *tv; ///< Location where conversion result is saved.
+ bool container; ///< True if tv is a container.
+ bool special; ///< If true then tv is a _VAL part of special dictionary
+ ///< that represents mapping.
+ int idx; ///< Container index (used to detect self-referencing structures).
+} TVPopStackItem;
+
+/// Convert lua object to VimL typval_T
+///
+/// Should pop exactly one value from lua stack.
+///
+/// @param lstate Lua state.
+/// @param[out] ret_tv Where to put the result.
+///
+/// @return `true` in case of success, `false` in case of failure. Error is
+/// reported automatically.
+bool nlua_pop_typval(lua_State *lstate, typval_T *ret_tv)
+{
+ bool ret = true;
+ const int initial_size = lua_gettop(lstate);
+ kvec_t(TVPopStackItem) stack = KV_INITIAL_VALUE;
+ kv_push(stack, ((TVPopStackItem) { ret_tv, false, false, 0 }));
+ while (ret && kv_size(stack)) {
+ if (!lua_checkstack(lstate, lua_gettop(lstate) + 3)) {
+ emsgf(_("E1502: Lua failed to grow stack to %i"), lua_gettop(lstate) + 3);
+ ret = false;
+ break;
+ }
+ TVPopStackItem cur = kv_pop(stack);
+ if (cur.container) {
+ if (cur.special || cur.tv->v_type == VAR_DICT) {
+ assert(cur.tv->v_type == (cur.special ? VAR_LIST : VAR_DICT));
+ bool next_key_found = false;
+ while (lua_next(lstate, -2)) {
+ if (lua_type(lstate, -2) == LUA_TSTRING) {
+ next_key_found = true;
+ break;
+ }
+ lua_pop(lstate, 1);
+ }
+ if (next_key_found) {
+ size_t len;
+ const char *s = lua_tolstring(lstate, -2, &len);
+ if (cur.special) {
+ list_T *const kv_pair = tv_list_alloc();
+ tv_list_append_list(cur.tv->vval.v_list, kv_pair);
+ listitem_T *const key = tv_list_item_alloc();
+ key->li_tv = decode_string(s, len, kTrue, false, false);
+ tv_list_append(kv_pair, key);
+ if (key->li_tv.v_type == VAR_UNKNOWN) {
+ ret = false;
+ tv_list_unref(kv_pair);
+ continue;
+ }
+ listitem_T *const val = tv_list_item_alloc();
+ tv_list_append(kv_pair, val);
+ kv_push(stack, cur);
+ cur = (TVPopStackItem) { &val->li_tv, false, false, 0 };
+ } else {
+ dictitem_T *const di = tv_dict_item_alloc_len(s, len);
+ if (tv_dict_add(cur.tv->vval.v_dict, di) == FAIL) {
+ assert(false);
+ }
+ kv_push(stack, cur);
+ cur = (TVPopStackItem) { &di->di_tv, false, false, 0 };
+ }
+ } else {
+ lua_pop(lstate, 1);
+ continue;
+ }
+ } else {
+ assert(cur.tv->v_type == VAR_LIST);
+ lua_rawgeti(lstate, -1, cur.tv->vval.v_list->lv_len + 1);
+ if (lua_isnil(lstate, -1)) {
+ lua_pop(lstate, 2);
+ continue;
+ }
+ listitem_T *const li = tv_list_item_alloc();
+ tv_list_append(cur.tv->vval.v_list, li);
+ kv_push(stack, cur);
+ cur = (TVPopStackItem) { &li->li_tv, false, false, 0 };
+ }
+ }
+ assert(!cur.container);
+ *cur.tv = (typval_T) {
+ .v_type = VAR_NUMBER,
+ .v_lock = VAR_UNLOCKED,
+ .vval = { .v_number = 0 },
+ };
+ switch (lua_type(lstate, -1)) {
+ case LUA_TNIL: {
+ cur.tv->v_type = VAR_SPECIAL;
+ cur.tv->vval.v_special = kSpecialVarNull;
+ break;
+ }
+ case LUA_TBOOLEAN: {
+ cur.tv->v_type = VAR_SPECIAL;
+ cur.tv->vval.v_special = (lua_toboolean(lstate, -1)
+ ? kSpecialVarTrue
+ : kSpecialVarFalse);
+ break;
+ }
+ case LUA_TSTRING: {
+ size_t len;
+ const char *s = lua_tolstring(lstate, -1, &len);
+ *cur.tv = decode_string(s, len, kNone, true, false);
+ if (cur.tv->v_type == VAR_UNKNOWN) {
+ ret = false;
+ }
+ break;
+ }
+ case LUA_TNUMBER: {
+ const lua_Number n = lua_tonumber(lstate, -1);
+ if (n > (lua_Number)VARNUMBER_MAX || n < (lua_Number)VARNUMBER_MIN
+ || ((lua_Number)((varnumber_T)n)) != n) {
+ cur.tv->v_type = VAR_FLOAT;
+ cur.tv->vval.v_float = (float_T)n;
+ } else {
+ cur.tv->v_type = VAR_NUMBER;
+ cur.tv->vval.v_number = (varnumber_T)n;
+ }
+ break;
+ }
+ case LUA_TTABLE: {
+ const LuaTableProps table_props = nlua_traverse_table(lstate);
+
+ for (size_t i = 0; i < kv_size(stack); i++) {
+ const TVPopStackItem item = kv_A(stack, i);
+ if (item.container && lua_rawequal(lstate, -1, item.idx)) {
+ tv_copy(item.tv, cur.tv);
+ cur.container = false;
+ goto nlua_pop_typval_table_processing_end;
+ }
+ }
+
+ switch (table_props.type) {
+ case kObjectTypeArray: {
+ cur.tv->v_type = VAR_LIST;
+ cur.tv->vval.v_list = tv_list_alloc();
+ cur.tv->vval.v_list->lv_refcount++;
+ if (table_props.maxidx != 0) {
+ cur.container = true;
+ cur.idx = lua_gettop(lstate);
+ kv_push(stack, cur);
+ }
+ break;
+ }
+ case kObjectTypeDictionary: {
+ if (table_props.string_keys_num == 0) {
+ cur.tv->v_type = VAR_DICT;
+ cur.tv->vval.v_dict = tv_dict_alloc();
+ cur.tv->vval.v_dict->dv_refcount++;
+ } else {
+ cur.special = table_props.has_string_with_nul;
+ if (table_props.has_string_with_nul) {
+ decode_create_map_special_dict(cur.tv);
+ assert(cur.tv->v_type = VAR_DICT);
+ dictitem_T *const val_di = tv_dict_find(cur.tv->vval.v_dict,
+ S_LEN("_VAL"));
+ assert(val_di != NULL);
+ cur.tv = &val_di->di_tv;
+ assert(cur.tv->v_type == VAR_LIST);
+ } else {
+ cur.tv->v_type = VAR_DICT;
+ cur.tv->vval.v_dict = tv_dict_alloc();
+ cur.tv->vval.v_dict->dv_refcount++;
+ }
+ cur.container = true;
+ cur.idx = lua_gettop(lstate);
+ kv_push(stack, cur);
+ lua_pushnil(lstate);
+ }
+ break;
+ }
+ case kObjectTypeFloat: {
+ cur.tv->v_type = VAR_FLOAT;
+ cur.tv->vval.v_float = (float_T)table_props.val;
+ break;
+ }
+ case kObjectTypeNil: {
+ EMSG(_("E5100: Cannot convert given lua table: table "
+ "should either have a sequence of positive integer keys "
+ "or contain only string keys"));
+ ret = false;
+ break;
+ }
+ default: {
+ assert(false);
+ }
+ }
+nlua_pop_typval_table_processing_end:
+ break;
+ }
+ default: {
+ EMSG(_("E5101: Cannot convert given lua type"));
+ ret = false;
+ break;
+ }
+ }
+ if (!cur.container) {
+ lua_pop(lstate, 1);
+ }
+ }
+ kv_destroy(stack);
+ if (!ret) {
+ tv_clear(ret_tv);
+ *ret_tv = (typval_T) {
+ .v_type = VAR_NUMBER,
+ .v_lock = VAR_UNLOCKED,
+ .vval = { .v_number = 0 },
+ };
+ lua_pop(lstate, lua_gettop(lstate) - initial_size + 1);
+ }
+ assert(lua_gettop(lstate) == initial_size - 1);
+ return ret;
+}
+
+#define TYPVAL_ENCODE_ALLOW_SPECIALS true
+
+#define TYPVAL_ENCODE_CONV_NIL(tv) \
+ lua_pushnil(lstate)
+
+#define TYPVAL_ENCODE_CONV_BOOL(tv, num) \
+ lua_pushboolean(lstate, (bool)(num))
+
+#define TYPVAL_ENCODE_CONV_NUMBER(tv, num) \
+ lua_pushnumber(lstate, (lua_Number)(num))
+
+#define TYPVAL_ENCODE_CONV_UNSIGNED_NUMBER TYPVAL_ENCODE_CONV_NUMBER
+
+#define TYPVAL_ENCODE_CONV_FLOAT(tv, flt) \
+ TYPVAL_ENCODE_CONV_NUMBER(tv, flt)
+
+#define TYPVAL_ENCODE_CONV_STRING(tv, str, len) \
+ lua_pushlstring(lstate, (const char *)(str), (len))
+
+#define TYPVAL_ENCODE_CONV_STR_STRING TYPVAL_ENCODE_CONV_STRING
+
+#define TYPVAL_ENCODE_CONV_EXT_STRING(tv, str, len, type) \
+ TYPVAL_ENCODE_CONV_NIL()
+
+#define TYPVAL_ENCODE_CONV_FUNC_START(tv, fun) \
+ do { \
+ TYPVAL_ENCODE_CONV_NIL(tv); \
+ goto typval_encode_stop_converting_one_item; \
+ } while (0)
+
+#define TYPVAL_ENCODE_CONV_FUNC_BEFORE_ARGS(tv, len)
+#define TYPVAL_ENCODE_CONV_FUNC_BEFORE_SELF(tv, len)
+#define TYPVAL_ENCODE_CONV_FUNC_END(tv)
+
+#define TYPVAL_ENCODE_CONV_EMPTY_LIST(tv) \
+ lua_createtable(lstate, 0, 0)
+
+#define TYPVAL_ENCODE_CONV_EMPTY_DICT(tv, dict) \
+ nlua_create_typed_table(lstate, 0, 0, kObjectTypeDictionary)
+
+#define TYPVAL_ENCODE_CONV_LIST_START(tv, len) \
+ do { \
+ if (!lua_checkstack(lstate, lua_gettop(lstate) + 3)) { \
+ emsgf(_("E5102: Lua failed to grow stack to %i"), \
+ lua_gettop(lstate) + 3); \
+ return false; \
+ } \
+ lua_createtable(lstate, (int)(len), 0); \
+ lua_pushnumber(lstate, 1); \
+ } while (0)
+
+#define TYPVAL_ENCODE_CONV_REAL_LIST_AFTER_START(tv, mpsv)
+
+#define TYPVAL_ENCODE_CONV_LIST_BETWEEN_ITEMS(tv) \
+ do { \
+ lua_Number idx = lua_tonumber(lstate, -2); \
+ lua_rawset(lstate, -3); \
+ lua_pushnumber(lstate, idx + 1); \
+ } while (0)
+
+#define TYPVAL_ENCODE_CONV_LIST_END(tv) \
+ lua_rawset(lstate, -3)
+
+#define TYPVAL_ENCODE_CONV_DICT_START(tv, dict, len) \
+ do { \
+ if (!lua_checkstack(lstate, lua_gettop(lstate) + 3)) { \
+ emsgf(_("E5102: Lua failed to grow stack to %i"), \
+ lua_gettop(lstate) + 3); \
+ return false; \
+ } \
+ lua_createtable(lstate, 0, (int)(len)); \
+ } while (0)
+
+#define TYPVAL_ENCODE_SPECIAL_DICT_KEY_CHECK(label, kv_pair)
+
+#define TYPVAL_ENCODE_CONV_REAL_DICT_AFTER_START(tv, dict, mpsv)
+
+#define TYPVAL_ENCODE_CONV_DICT_AFTER_KEY(tv, dict)
+
+#define TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS(tv, dict) \
+ lua_rawset(lstate, -3)
+
+#define TYPVAL_ENCODE_CONV_DICT_END(tv, dict) \
+ TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS(tv, dict)
+
+#define TYPVAL_ENCODE_CONV_RECURSE(val, conv_type) \
+ do { \
+ for (size_t backref = kv_size(*mpstack); backref; backref--) { \
+ const MPConvStackVal mpval = kv_A(*mpstack, backref - 1); \
+ if (mpval.type == conv_type) { \
+ if (conv_type == kMPConvDict \
+ ? (void *)mpval.data.d.dict == (void *)(val) \
+ : (void *)mpval.data.l.list == (void *)(val)) { \
+ lua_pushvalue(lstate, \
+ -((int)((kv_size(*mpstack) - backref + 1) * 2))); \
+ break; \
+ } \
+ } \
+ } \
+ } while (0)
+
+#define TYPVAL_ENCODE_SCOPE static
+#define TYPVAL_ENCODE_NAME lua
+#define TYPVAL_ENCODE_FIRST_ARG_TYPE lua_State *const
+#define TYPVAL_ENCODE_FIRST_ARG_NAME lstate
+#include "nvim/eval/typval_encode.c.h"
+#undef TYPVAL_ENCODE_SCOPE
+#undef TYPVAL_ENCODE_NAME
+#undef TYPVAL_ENCODE_FIRST_ARG_TYPE
+#undef TYPVAL_ENCODE_FIRST_ARG_NAME
+
+#undef TYPVAL_ENCODE_CONV_STRING
+#undef TYPVAL_ENCODE_CONV_STR_STRING
+#undef TYPVAL_ENCODE_CONV_EXT_STRING
+#undef TYPVAL_ENCODE_CONV_NUMBER
+#undef TYPVAL_ENCODE_CONV_FLOAT
+#undef TYPVAL_ENCODE_CONV_FUNC_START
+#undef TYPVAL_ENCODE_CONV_FUNC_BEFORE_ARGS
+#undef TYPVAL_ENCODE_CONV_FUNC_BEFORE_SELF
+#undef TYPVAL_ENCODE_CONV_FUNC_END
+#undef TYPVAL_ENCODE_CONV_EMPTY_LIST
+#undef TYPVAL_ENCODE_CONV_LIST_START
+#undef TYPVAL_ENCODE_CONV_REAL_LIST_AFTER_START
+#undef TYPVAL_ENCODE_CONV_EMPTY_DICT
+#undef TYPVAL_ENCODE_CONV_NIL
+#undef TYPVAL_ENCODE_CONV_BOOL
+#undef TYPVAL_ENCODE_CONV_UNSIGNED_NUMBER
+#undef TYPVAL_ENCODE_CONV_DICT_START
+#undef TYPVAL_ENCODE_CONV_REAL_DICT_AFTER_START
+#undef TYPVAL_ENCODE_CONV_DICT_END
+#undef TYPVAL_ENCODE_CONV_DICT_AFTER_KEY
+#undef TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS
+#undef TYPVAL_ENCODE_SPECIAL_DICT_KEY_CHECK
+#undef TYPVAL_ENCODE_CONV_LIST_END
+#undef TYPVAL_ENCODE_CONV_LIST_BETWEEN_ITEMS
+#undef TYPVAL_ENCODE_CONV_RECURSE
+#undef TYPVAL_ENCODE_ALLOW_SPECIALS
+
+/// Convert VimL typval_T to lua value
+///
+/// Should leave single value in lua stack. May only fail if lua failed to grow
+/// stack.
+///
+/// @param lstate Lua interpreter state.
+/// @param[in] tv typval_T to convert.
+///
+/// @return true in case of success, false otherwise.
+bool nlua_push_typval(lua_State *lstate, typval_T *const tv)
+{
+ const int initial_size = lua_gettop(lstate);
+ if (!lua_checkstack(lstate, initial_size + 2)) {
+ emsgf(_("E1502: Lua failed to grow stack to %i"), initial_size + 4);
+ return false;
+ }
+ if (encode_vim_to_lua(lstate, tv, "nlua_push_typval argument") == FAIL) {
+ return false;
+ }
+ assert(lua_gettop(lstate) == initial_size + 1);
+ return true;
+}
+
+/// Push value which is a type index
+///
+/// Used for all “typed” tables: i.e. for all tables which represent VimL
+/// values.
+static inline void nlua_push_type_idx(lua_State *lstate)
+ FUNC_ATTR_NONNULL_ALL
+{
+ lua_pushboolean(lstate, TYPE_IDX_VALUE);
+}
+
+/// Push value which is a value index
+///
+/// Used for tables which represent scalar values, like float value.
+static inline void nlua_push_val_idx(lua_State *lstate)
+ FUNC_ATTR_NONNULL_ALL
+{
+ lua_pushboolean(lstate, VAL_IDX_VALUE);
+}
+
+/// Push type
+///
+/// Type is a value in vim.types table.
+///
+/// @param[out] lstate Lua state.
+/// @param[in] type Type to push.
+static inline void nlua_push_type(lua_State *lstate, ObjectType type)
+ FUNC_ATTR_NONNULL_ALL
+{
+ lua_pushnumber(lstate, (lua_Number)type);
+}
+
+/// Create lua table which has an entry that determines its VimL type
+///
+/// @param[out] lstate Lua state.
+/// @param[in] narr Number of “array” entries to be populated later.
+/// @param[in] nrec Number of “dictionary” entries to be populated later.
+/// @param[in] type Type of the table.
+static inline void nlua_create_typed_table(lua_State *lstate,
+ const size_t narr,
+ const size_t nrec,
+ const ObjectType type)
+ FUNC_ATTR_NONNULL_ALL
+{
+ lua_createtable(lstate, (int)narr, (int)(1 + nrec));
+ nlua_push_type_idx(lstate);
+ nlua_push_type(lstate, type);
+ lua_rawset(lstate, -3);
+}
+
+
+/// Convert given String to lua string
+///
+/// Leaves converted string on top of the stack.
+void nlua_push_String(lua_State *lstate, const String s)
+ FUNC_ATTR_NONNULL_ALL
+{
+ lua_pushlstring(lstate, s.data, s.size);
+}
+
+/// Convert given Integer to lua number
+///
+/// Leaves converted number on top of the stack.
+void nlua_push_Integer(lua_State *lstate, const Integer n)
+ FUNC_ATTR_NONNULL_ALL
+{
+ lua_pushnumber(lstate, (lua_Number)n);
+}
+
+/// Convert given Float to lua table
+///
+/// Leaves converted table on top of the stack.
+void nlua_push_Float(lua_State *lstate, const Float f)
+ FUNC_ATTR_NONNULL_ALL
+{
+ nlua_create_typed_table(lstate, 0, 1, kObjectTypeFloat);
+ nlua_push_val_idx(lstate);
+ lua_pushnumber(lstate, (lua_Number)f);
+ lua_rawset(lstate, -3);
+}
+
+/// Convert given Float to lua boolean
+///
+/// Leaves converted value on top of the stack.
+void nlua_push_Boolean(lua_State *lstate, const Boolean b)
+ FUNC_ATTR_NONNULL_ALL
+{
+ lua_pushboolean(lstate, b);
+}
+
+/// Convert given Dictionary to lua table
+///
+/// Leaves converted table on top of the stack.
+void nlua_push_Dictionary(lua_State *lstate, const Dictionary dict)
+ FUNC_ATTR_NONNULL_ALL
+{
+ if (dict.size == 0) {
+ nlua_create_typed_table(lstate, 0, 0, kObjectTypeDictionary);
+ } else {
+ lua_createtable(lstate, 0, (int)dict.size);
+ }
+ for (size_t i = 0; i < dict.size; i++) {
+ nlua_push_String(lstate, dict.items[i].key);
+ nlua_push_Object(lstate, dict.items[i].value);
+ lua_rawset(lstate, -3);
+ }
+}
+
+/// Convert given Array to lua table
+///
+/// Leaves converted table on top of the stack.
+void nlua_push_Array(lua_State *lstate, const Array array)
+ FUNC_ATTR_NONNULL_ALL
+{
+ lua_createtable(lstate, (int)array.size, 0);
+ for (size_t i = 0; i < array.size; i++) {
+ nlua_push_Object(lstate, array.items[i]);
+ lua_rawseti(lstate, -2, (int)i + 1);
+ }
+}
+
+#define GENERATE_INDEX_FUNCTION(type) \
+void nlua_push_##type(lua_State *lstate, const type item) \
+ FUNC_ATTR_NONNULL_ALL \
+{ \
+ lua_pushnumber(lstate, (lua_Number)(item)); \
+}
+
+GENERATE_INDEX_FUNCTION(Buffer)
+GENERATE_INDEX_FUNCTION(Window)
+GENERATE_INDEX_FUNCTION(Tabpage)
+
+#undef GENERATE_INDEX_FUNCTION
+
+/// Convert given Object to lua value
+///
+/// Leaves converted value on top of the stack.
+void nlua_push_Object(lua_State *lstate, const Object obj)
+ FUNC_ATTR_NONNULL_ALL
+{
+ switch (obj.type) {
+ case kObjectTypeNil: {
+ lua_pushnil(lstate);
+ break;
+ }
+#define ADD_TYPE(type, data_key) \
+ case kObjectType##type: { \
+ nlua_push_##type(lstate, obj.data.data_key); \
+ break; \
+ }
+ ADD_TYPE(Boolean, boolean)
+ ADD_TYPE(Integer, integer)
+ ADD_TYPE(Float, floating)
+ ADD_TYPE(String, string)
+ ADD_TYPE(Array, array)
+ ADD_TYPE(Dictionary, dictionary)
+#undef ADD_TYPE
+#define ADD_REMOTE_TYPE(type) \
+ case kObjectType##type: { \
+ nlua_push_##type(lstate, (type)obj.data.integer); \
+ break; \
+ }
+ ADD_REMOTE_TYPE(Buffer)
+ ADD_REMOTE_TYPE(Window)
+ ADD_REMOTE_TYPE(Tabpage)
+#undef ADD_REMOTE_TYPE
+ }
+}
+
+
+/// Convert lua value to string
+///
+/// Always pops one value from the stack.
+String nlua_pop_String(lua_State *lstate, Error *err)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ if (lua_type(lstate, -1) != LUA_TSTRING) {
+ lua_pop(lstate, 1);
+ api_set_error(err, kErrorTypeValidation, "Expected lua string");
+ return (String) { .size = 0, .data = NULL };
+ }
+ String ret;
+
+ ret.data = (char *)lua_tolstring(lstate, -1, &(ret.size));
+ assert(ret.data != NULL);
+ ret.data = xmemdupz(ret.data, ret.size);
+ lua_pop(lstate, 1);
+
+ return ret;
+}
+
+/// Convert lua value to integer
+///
+/// Always pops one value from the stack.
+Integer nlua_pop_Integer(lua_State *lstate, Error *err)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ if (lua_type(lstate, -1) != LUA_TNUMBER) {
+ lua_pop(lstate, 1);
+ api_set_error(err, kErrorTypeValidation, "Expected lua number");
+ return 0;
+ }
+ const lua_Number n = lua_tonumber(lstate, -1);
+ lua_pop(lstate, 1);
+ if (n > (lua_Number)API_INTEGER_MAX || n < (lua_Number)API_INTEGER_MIN
+ || ((lua_Number)((Integer)n)) != n) {
+ api_set_error(err, kErrorTypeException, "Number is not integral");
+ return 0;
+ }
+ return (Integer)n;
+}
+
+/// Convert lua value to boolean
+///
+/// Always pops one value from the stack.
+Boolean nlua_pop_Boolean(lua_State *lstate, Error *err)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ const Boolean ret = lua_toboolean(lstate, -1);
+ lua_pop(lstate, 1);
+ return ret;
+}
+
+/// Check whether typed table on top of the stack has given type
+///
+/// @param[in] lstate Lua state.
+/// @param[out] err Location where error will be saved. May be NULL.
+/// @param[in] type Type to check.
+///
+/// @return @see nlua_traverse_table().
+static inline LuaTableProps nlua_check_type(lua_State *const lstate,
+ Error *const err,
+ const ObjectType type)
+ FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ if (lua_type(lstate, -1) != LUA_TTABLE) {
+ if (err) {
+ api_set_error(err, kErrorTypeValidation, "Expected lua table");
+ }
+ return (LuaTableProps) { .type = kObjectTypeNil };
+ }
+ LuaTableProps table_props = nlua_traverse_table(lstate);
+
+ if (type == kObjectTypeDictionary && table_props.type == kObjectTypeArray
+ && table_props.maxidx == 0 && !table_props.has_type_key) {
+ table_props.type = kObjectTypeDictionary;
+ }
+
+ if (table_props.type != type) {
+ if (err) {
+ api_set_error(err, kErrorTypeValidation, "Unexpected type");
+ }
+ }
+
+ return table_props;
+}
+
+/// Convert lua table to float
+///
+/// Always pops one value from the stack.
+Float nlua_pop_Float(lua_State *lstate, Error *err)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ if (lua_type(lstate, -1) == LUA_TNUMBER) {
+ const Float ret = (Float)lua_tonumber(lstate, -1);
+ lua_pop(lstate, 1);
+ return ret;
+ }
+
+ const LuaTableProps table_props = nlua_check_type(lstate, err,
+ kObjectTypeFloat);
+ lua_pop(lstate, 1);
+ if (table_props.type != kObjectTypeFloat) {
+ return 0;
+ } else {
+ return (Float)table_props.val;
+ }
+}
+
+/// Convert lua table to array without determining whether it is array
+///
+/// @param lstate Lua state.
+/// @param[in] table_props nlua_traverse_table() output.
+/// @param[out] err Location where error will be saved.
+static Array nlua_pop_Array_unchecked(lua_State *const lstate,
+ const LuaTableProps table_props,
+ Error *const err)
+{
+ Array ret = { .size = table_props.maxidx, .items = NULL };
+
+ if (ret.size == 0) {
+ lua_pop(lstate, 1);
+ return ret;
+ }
+
+ ret.items = xcalloc(ret.size, sizeof(*ret.items));
+ for (size_t i = 1; i <= ret.size; i++) {
+ Object val;
+
+ lua_rawgeti(lstate, -1, (int)i);
+
+ val = nlua_pop_Object(lstate, err);
+ if (ERROR_SET(err)) {
+ ret.size = i - 1;
+ lua_pop(lstate, 1);
+ api_free_array(ret);
+ return (Array) { .size = 0, .items = NULL };
+ }
+ ret.items[i - 1] = val;
+ }
+ lua_pop(lstate, 1);
+
+ return ret;
+}
+
+/// Convert lua table to array
+///
+/// Always pops one value from the stack.
+Array nlua_pop_Array(lua_State *lstate, Error *err)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ const LuaTableProps table_props = nlua_check_type(lstate, err,
+ kObjectTypeArray);
+ if (table_props.type != kObjectTypeArray) {
+ return (Array) { .size = 0, .items = NULL };
+ }
+ return nlua_pop_Array_unchecked(lstate, table_props, err);
+}
+
+/// Convert lua table to dictionary
+///
+/// Always pops one value from the stack. Does not check whether whether topmost
+/// value on the stack is a table.
+///
+/// @param lstate Lua interpreter state.
+/// @param[in] table_props nlua_traverse_table() output.
+/// @param[out] err Location where error will be saved.
+static Dictionary nlua_pop_Dictionary_unchecked(lua_State *lstate,
+ const LuaTableProps table_props,
+ Error *err)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ Dictionary ret = { .size = table_props.string_keys_num, .items = NULL };
+
+ if (ret.size == 0) {
+ lua_pop(lstate, 1);
+ return ret;
+ }
+ ret.items = xcalloc(ret.size, sizeof(*ret.items));
+
+ lua_pushnil(lstate);
+ for (size_t i = 0; lua_next(lstate, -2) && i < ret.size;) {
+ // stack: dict, key, value
+
+ if (lua_type(lstate, -2) == LUA_TSTRING) {
+ lua_pushvalue(lstate, -2);
+ // stack: dict, key, value, key
+
+ ret.items[i].key = nlua_pop_String(lstate, err);
+ // stack: dict, key, value
+
+ if (!ERROR_SET(err)) {
+ ret.items[i].value = nlua_pop_Object(lstate, err);
+ // stack: dict, key
+ } else {
+ lua_pop(lstate, 1);
+ // stack: dict, key
+ }
+
+ if (ERROR_SET(err)) {
+ ret.size = i;
+ api_free_dictionary(ret);
+ lua_pop(lstate, 2);
+ // stack:
+ return (Dictionary) { .size = 0, .items = NULL };
+ }
+ i++;
+ } else {
+ lua_pop(lstate, 1);
+ // stack: dict, key
+ }
+ }
+ lua_pop(lstate, 1);
+
+ return ret;
+}
+
+/// Convert lua table to dictionary
+///
+/// Always pops one value from the stack.
+Dictionary nlua_pop_Dictionary(lua_State *lstate, Error *err)
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ const LuaTableProps table_props = nlua_check_type(lstate, err,
+ kObjectTypeDictionary);
+ if (table_props.type != kObjectTypeDictionary) {
+ lua_pop(lstate, 1);
+ return (Dictionary) { .size = 0, .items = NULL };
+ }
+
+ return nlua_pop_Dictionary_unchecked(lstate, table_props, err);
+}
+
+/// Helper structure for nlua_pop_Object
+typedef struct {
+ Object *obj; ///< Location where conversion result is saved.
+ bool container; ///< True if tv is a container.
+} ObjPopStackItem;
+
+/// Convert lua table to object
+///
+/// Always pops one value from the stack.
+Object nlua_pop_Object(lua_State *const lstate, Error *const err)
+{
+ Object ret = NIL;
+ const int initial_size = lua_gettop(lstate);
+ kvec_t(ObjPopStackItem) stack = KV_INITIAL_VALUE;
+ kv_push(stack, ((ObjPopStackItem) { &ret, false }));
+ while (!ERROR_SET(err) && kv_size(stack)) {
+ if (!lua_checkstack(lstate, lua_gettop(lstate) + 3)) {
+ api_set_error(err, kErrorTypeException, "Lua failed to grow stack");
+ break;
+ }
+ ObjPopStackItem cur = kv_pop(stack);
+ if (cur.container) {
+ if (cur.obj->type == kObjectTypeDictionary) {
+ // stack: …, dict, key
+ if (cur.obj->data.dictionary.size
+ == cur.obj->data.dictionary.capacity) {
+ lua_pop(lstate, 2);
+ continue;
+ }
+ bool next_key_found = false;
+ while (lua_next(lstate, -2)) {
+ // stack: …, dict, new key, val
+ if (lua_type(lstate, -2) == LUA_TSTRING) {
+ next_key_found = true;
+ break;
+ }
+ lua_pop(lstate, 1);
+ // stack: …, dict, new key
+ }
+ if (next_key_found) {
+ // stack: …, dict, new key, val
+ size_t len;
+ const char *s = lua_tolstring(lstate, -2, &len);
+ const size_t idx = cur.obj->data.dictionary.size++;
+ cur.obj->data.dictionary.items[idx].key = (String) {
+ .data = xmemdupz(s, len),
+ .size = len,
+ };
+ kv_push(stack, cur);
+ cur = (ObjPopStackItem) {
+ .obj = &cur.obj->data.dictionary.items[idx].value,
+ .container = false,
+ };
+ } else {
+ // stack: …, dict
+ lua_pop(lstate, 1);
+ // stack: …
+ continue;
+ }
+ } else {
+ if (cur.obj->data.array.size == cur.obj->data.array.capacity) {
+ lua_pop(lstate, 1);
+ continue;
+ }
+ const size_t idx = cur.obj->data.array.size++;
+ lua_rawgeti(lstate, -1, (int)idx + 1);
+ if (lua_isnil(lstate, -1)) {
+ lua_pop(lstate, 2);
+ continue;
+ }
+ kv_push(stack, cur);
+ cur = (ObjPopStackItem) {
+ .obj = &cur.obj->data.array.items[idx],
+ .container = false,
+ };
+ }
+ }
+ assert(!cur.container);
+ *cur.obj = NIL;
+ switch (lua_type(lstate, -1)) {
+ case LUA_TNIL: {
+ break;
+ }
+ case LUA_TBOOLEAN: {
+ *cur.obj = BOOLEAN_OBJ(lua_toboolean(lstate, -1));
+ break;
+ }
+ case LUA_TSTRING: {
+ size_t len;
+ const char *s = lua_tolstring(lstate, -1, &len);
+ *cur.obj = STRING_OBJ(((String) {
+ .data = xmemdupz(s, len),
+ .size = len,
+ }));
+ break;
+ }
+ case LUA_TNUMBER: {
+ const lua_Number n = lua_tonumber(lstate, -1);
+ if (n > (lua_Number)API_INTEGER_MAX || n < (lua_Number)API_INTEGER_MIN
+ || ((lua_Number)((Integer)n)) != n) {
+ *cur.obj = FLOAT_OBJ((Float)n);
+ } else {
+ *cur.obj = INTEGER_OBJ((Integer)n);
+ }
+ break;
+ }
+ case LUA_TTABLE: {
+ const LuaTableProps table_props = nlua_traverse_table(lstate);
+
+ switch (table_props.type) {
+ case kObjectTypeArray: {
+ *cur.obj = ARRAY_OBJ(((Array) {
+ .items = NULL,
+ .size = 0,
+ .capacity = 0,
+ }));
+ if (table_props.maxidx != 0) {
+ cur.obj->data.array.items =
+ xcalloc(table_props.maxidx,
+ sizeof(cur.obj->data.array.items[0]));
+ cur.obj->data.array.capacity = table_props.maxidx;
+ cur.container = true;
+ kv_push(stack, cur);
+ }
+ break;
+ }
+ case kObjectTypeDictionary: {
+ *cur.obj = DICTIONARY_OBJ(((Dictionary) {
+ .items = NULL,
+ .size = 0,
+ .capacity = 0,
+ }));
+ if (table_props.string_keys_num != 0) {
+ cur.obj->data.dictionary.items =
+ xcalloc(table_props.string_keys_num,
+ sizeof(cur.obj->data.dictionary.items[0]));
+ cur.obj->data.dictionary.capacity = table_props.string_keys_num;
+ cur.container = true;
+ kv_push(stack, cur);
+ lua_pushnil(lstate);
+ }
+ break;
+ }
+ case kObjectTypeFloat: {
+ *cur.obj = FLOAT_OBJ((Float)table_props.val);
+ break;
+ }
+ case kObjectTypeNil: {
+ api_set_error(err, kErrorTypeValidation,
+ "Cannot convert given lua table");
+ break;
+ }
+ default: {
+ assert(false);
+ }
+ }
+ break;
+ }
+ default: {
+ api_set_error(err, kErrorTypeValidation,
+ "Cannot convert given lua type");
+ break;
+ }
+ }
+ if (!cur.container) {
+ lua_pop(lstate, 1);
+ }
+ }
+ kv_destroy(stack);
+ if (ERROR_SET(err)) {
+ api_free_object(ret);
+ ret = NIL;
+ lua_pop(lstate, lua_gettop(lstate) - initial_size + 1);
+ }
+ assert(lua_gettop(lstate) == initial_size - 1);
+ return ret;
+}
+
+#define GENERATE_INDEX_FUNCTION(type) \
+type nlua_pop_##type(lua_State *lstate, Error *err) \
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT \
+{ \
+ type ret; \
+ ret = (type)lua_tonumber(lstate, -1); \
+ lua_pop(lstate, 1); \
+ return ret; \
+}
+
+GENERATE_INDEX_FUNCTION(Buffer)
+GENERATE_INDEX_FUNCTION(Window)
+GENERATE_INDEX_FUNCTION(Tabpage)
+
+#undef GENERATE_INDEX_FUNCTION
+
+/// Record some auxilary values in vim module
+///
+/// Assumes that module table is on top of the stack.
+///
+/// Recorded values:
+///
+/// `vim.type_idx`: @see nlua_push_type_idx()
+/// `vim.val_idx`: @see nlua_push_val_idx()
+/// `vim.types`: table mapping possible values of `vim.type_idx` to string
+/// names (i.e. `array`, `float`, `dictionary`) and back.
+void nlua_init_types(lua_State *const lstate)
+{
+ LUA_PUSH_STATIC_STRING(lstate, "type_idx");
+ nlua_push_type_idx(lstate);
+ lua_rawset(lstate, -3);
+
+ LUA_PUSH_STATIC_STRING(lstate, "val_idx");
+ nlua_push_val_idx(lstate);
+ lua_rawset(lstate, -3);
+
+ LUA_PUSH_STATIC_STRING(lstate, "types");
+ lua_createtable(lstate, 0, 3);
+
+ LUA_PUSH_STATIC_STRING(lstate, "float");
+ lua_pushnumber(lstate, (lua_Number)kObjectTypeFloat);
+ lua_rawset(lstate, -3);
+ lua_pushnumber(lstate, (lua_Number)kObjectTypeFloat);
+ LUA_PUSH_STATIC_STRING(lstate, "float");
+ lua_rawset(lstate, -3);
+
+ LUA_PUSH_STATIC_STRING(lstate, "array");
+ lua_pushnumber(lstate, (lua_Number)kObjectTypeArray);
+ lua_rawset(lstate, -3);
+ lua_pushnumber(lstate, (lua_Number)kObjectTypeArray);
+ LUA_PUSH_STATIC_STRING(lstate, "array");
+ lua_rawset(lstate, -3);
+
+ LUA_PUSH_STATIC_STRING(lstate, "dictionary");
+ lua_pushnumber(lstate, (lua_Number)kObjectTypeDictionary);
+ lua_rawset(lstate, -3);
+ lua_pushnumber(lstate, (lua_Number)kObjectTypeDictionary);
+ LUA_PUSH_STATIC_STRING(lstate, "dictionary");
+ lua_rawset(lstate, -3);
+
+ lua_rawset(lstate, -3);
+}
diff --git a/src/nvim/lua/converter.h b/src/nvim/lua/converter.h
new file mode 100644
index 0000000000..542c56ea3e
--- /dev/null
+++ b/src/nvim/lua/converter.h
@@ -0,0 +1,15 @@
+#ifndef NVIM_LUA_CONVERTER_H
+#define NVIM_LUA_CONVERTER_H
+
+#include <lua.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "nvim/api/private/defs.h"
+#include "nvim/func_attr.h"
+#include "nvim/eval.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "lua/converter.h.generated.h"
+#endif
+#endif // NVIM_LUA_CONVERTER_H
diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c
new file mode 100644
index 0000000000..7cf326aef5
--- /dev/null
+++ b/src/nvim/lua/executor.c
@@ -0,0 +1,576 @@
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+
+#include "nvim/misc1.h"
+#include "nvim/getchar.h"
+#include "nvim/garray.h"
+#include "nvim/func_attr.h"
+#include "nvim/api/private/defs.h"
+#include "nvim/api/private/helpers.h"
+#include "nvim/api/vim.h"
+#include "nvim/vim.h"
+#include "nvim/ex_getln.h"
+#include "nvim/message.h"
+#include "nvim/memline.h"
+#include "nvim/buffer_defs.h"
+#include "nvim/macros.h"
+#include "nvim/screen.h"
+#include "nvim/cursor.h"
+#include "nvim/undo.h"
+#include "nvim/ascii.h"
+
+#include "nvim/lua/executor.h"
+#include "nvim/lua/converter.h"
+
+typedef struct {
+ Error err;
+ String lua_err_str;
+} LuaError;
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "lua/vim_module.generated.h"
+# include "lua/executor.c.generated.h"
+#endif
+
+/// Name of the run code for use in messages
+#define NLUA_EVAL_NAME "<VimL compiled string>"
+
+/// Call C function which does not expect any arguments
+///
+/// @param function Called function
+/// @param numret Number of returned arguments
+#define NLUA_CALL_C_FUNCTION_0(lstate, function, numret) \
+ do { \
+ lua_pushcfunction(lstate, &function); \
+ lua_call(lstate, 0, numret); \
+ } while (0)
+/// Call C function which expects one argument
+///
+/// @param function Called function
+/// @param numret Number of returned arguments
+/// @param a… Supplied argument (should be a void* pointer)
+#define NLUA_CALL_C_FUNCTION_1(lstate, function, numret, a1) \
+ do { \
+ lua_pushcfunction(lstate, &function); \
+ lua_pushlightuserdata(lstate, a1); \
+ lua_call(lstate, 1, numret); \
+ } while (0)
+/// Call C function which expects two arguments
+///
+/// @param function Called function
+/// @param numret Number of returned arguments
+/// @param a… Supplied argument (should be a void* pointer)
+#define NLUA_CALL_C_FUNCTION_2(lstate, function, numret, a1, a2) \
+ do { \
+ lua_pushcfunction(lstate, &function); \
+ lua_pushlightuserdata(lstate, a1); \
+ lua_pushlightuserdata(lstate, a2); \
+ lua_call(lstate, 2, numret); \
+ } while (0)
+/// Call C function which expects three arguments
+///
+/// @param function Called function
+/// @param numret Number of returned arguments
+/// @param a… Supplied argument (should be a void* pointer)
+#define NLUA_CALL_C_FUNCTION_3(lstate, function, numret, a1, a2, a3) \
+ do { \
+ lua_pushcfunction(lstate, &function); \
+ lua_pushlightuserdata(lstate, a1); \
+ lua_pushlightuserdata(lstate, a2); \
+ lua_pushlightuserdata(lstate, a3); \
+ lua_call(lstate, 3, numret); \
+ } while (0)
+/// Call C function which expects five arguments
+///
+/// @param function Called function
+/// @param numret Number of returned arguments
+/// @param a… Supplied argument (should be a void* pointer)
+#define NLUA_CALL_C_FUNCTION_4(lstate, function, numret, a1, a2, a3, a4) \
+ do { \
+ lua_pushcfunction(lstate, &function); \
+ lua_pushlightuserdata(lstate, a1); \
+ lua_pushlightuserdata(lstate, a2); \
+ lua_pushlightuserdata(lstate, a3); \
+ lua_pushlightuserdata(lstate, a4); \
+ lua_call(lstate, 4, numret); \
+ } while (0)
+
+/// Convert lua error into a Vim error message
+///
+/// @param lstate Lua interpreter state.
+/// @param[in] msg Message base, must contain one `%s`.
+static void nlua_error(lua_State *const lstate, const char *const msg)
+ FUNC_ATTR_NONNULL_ALL
+{
+ size_t len;
+ const char *const str = lua_tolstring(lstate, -1, &len);
+
+ emsgf(msg, (int)len, str);
+
+ lua_pop(lstate, 1);
+}
+
+/// Compare two strings, ignoring case
+///
+/// Expects two values on the stack: compared strings. Returns one of the
+/// following numbers: 0, -1 or 1.
+///
+/// Does no error handling: never call it with non-string or with some arguments
+/// omitted.
+static int nlua_stricmp(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
+{
+ const char *s1 = luaL_checklstring(lstate, 1, NULL);
+ const char *s2 = luaL_checklstring(lstate, 2, NULL);
+ const int ret = STRICMP(s1, s2);
+ lua_pop(lstate, 2);
+ lua_pushnumber(lstate, (lua_Number)((ret > 0) - (ret < 0)));
+ return 1;
+}
+
+/// Evaluate lua string
+///
+/// Expects two values on the stack: string to evaluate, pointer to the
+/// location where result is saved. Always returns nothing (from the lua point
+/// of view).
+static int nlua_exec_lua_string(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
+{
+ const String *const str = (const String *)lua_touserdata(lstate, 1);
+ typval_T *const ret_tv = (typval_T *)lua_touserdata(lstate, 2);
+ lua_pop(lstate, 2);
+
+ if (luaL_loadbuffer(lstate, str->data, str->size, NLUA_EVAL_NAME)) {
+ nlua_error(lstate, _("E5104: Error while creating lua chunk: %.*s"));
+ return 0;
+ }
+ if (lua_pcall(lstate, 0, 1, 0)) {
+ nlua_error(lstate, _("E5105: Error while calling lua chunk: %.*s"));
+ return 0;
+ }
+ if (!nlua_pop_typval(lstate, ret_tv)) {
+ return 0;
+ }
+ return 0;
+}
+
+/// Evaluate lua string for each line in range
+///
+/// Expects two values on the stack: string to evaluate and pointer to integer
+/// array with line range. Always returns nothing (from the lua point of view).
+static int nlua_exec_luado_string(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
+{
+ const String *const str = (const String *)lua_touserdata(lstate, 1);
+ const linenr_T *const range = (const linenr_T *)lua_touserdata(lstate, 2);
+ lua_pop(lstate, 2);
+
+#define DOSTART "return function(line, linenr) "
+#define DOEND " end"
+ const size_t lcmd_len = (str->size
+ + (sizeof(DOSTART) - 1)
+ + (sizeof(DOEND) - 1));
+ char *lcmd;
+ if (lcmd_len < IOSIZE) {
+ lcmd = (char *)IObuff;
+ } else {
+ lcmd = xmalloc(lcmd_len + 1);
+ }
+ memcpy(lcmd, DOSTART, sizeof(DOSTART) - 1);
+ memcpy(lcmd + sizeof(DOSTART) - 1, str->data, str->size);
+ memcpy(lcmd + sizeof(DOSTART) - 1 + str->size, DOEND, sizeof(DOEND) - 1);
+#undef DOSTART
+#undef DOEND
+
+ if (luaL_loadbuffer(lstate, lcmd, lcmd_len, NLUA_EVAL_NAME)) {
+ nlua_error(lstate, _("E5109: Error while creating lua chunk: %.*s"));
+ if (lcmd_len >= IOSIZE) {
+ xfree(lcmd);
+ }
+ return 0;
+ }
+ if (lcmd_len >= IOSIZE) {
+ xfree(lcmd);
+ }
+ if (lua_pcall(lstate, 0, 1, 0)) {
+ nlua_error(lstate, _("E5110: Error while creating lua function: %.*s"));
+ return 0;
+ }
+ for (linenr_T l = range[0]; l <= range[1]; l++) {
+ if (l > curbuf->b_ml.ml_line_count) {
+ break;
+ }
+ lua_pushvalue(lstate, -1);
+ lua_pushstring(lstate, (const char *)ml_get_buf(curbuf, l, false));
+ lua_pushnumber(lstate, (lua_Number)l);
+ if (lua_pcall(lstate, 2, 1, 0)) {
+ nlua_error(lstate, _("E5111: Error while calling lua function: %.*s"));
+ break;
+ }
+ if (lua_isstring(lstate, -1)) {
+ size_t new_line_len;
+ const char *const new_line = lua_tolstring(lstate, -1, &new_line_len);
+ char *const new_line_transformed = xmemdupz(new_line, new_line_len);
+ for (size_t i = 0; i < new_line_len; i++) {
+ if (new_line_transformed[i] == NUL) {
+ new_line_transformed[i] = '\n';
+ }
+ }
+ ml_replace(l, (char_u *)new_line_transformed, false);
+ changed_bytes(l, 0);
+ }
+ lua_pop(lstate, 1);
+ }
+ lua_pop(lstate, 1);
+ check_cursor();
+ update_screen(NOT_VALID);
+ return 0;
+}
+
+/// Evaluate lua file
+///
+/// Expects one value on the stack: file to evaluate. Always returns nothing
+/// (from the lua point of view).
+static int nlua_exec_lua_file(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
+{
+ const char *const filename = (const char *)lua_touserdata(lstate, 1);
+ lua_pop(lstate, 1);
+
+ if (luaL_loadfile(lstate, filename)) {
+ nlua_error(lstate, _("E5112: Error while creating lua chunk: %.*s"));
+ return 0;
+ }
+ if (lua_pcall(lstate, 0, 0, 0)) {
+ nlua_error(lstate, _("E5113: Error while calling lua chunk: %.*s"));
+ return 0;
+ }
+ return 0;
+}
+
+/// Initialize lua interpreter state
+///
+/// Called by lua interpreter itself to initialize state.
+static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
+{
+ // stricmp
+ lua_pushcfunction(lstate, &nlua_stricmp);
+ lua_setglobal(lstate, "stricmp");
+
+ // print
+ lua_pushcfunction(lstate, &nlua_print);
+ lua_setglobal(lstate, "print");
+
+ // debug.debug
+ lua_getglobal(lstate, "debug");
+ lua_pushcfunction(lstate, &nlua_debug);
+ lua_setfield(lstate, -2, "debug");
+ lua_pop(lstate, 1);
+
+ // vim
+ if (luaL_dostring(lstate, (char *)&vim_module[0])) {
+ nlua_error(lstate, _("E5106: Error while creating vim module: %.*s"));
+ return 1;
+ }
+ // vim.api
+ nlua_add_api_functions(lstate);
+ // vim.types, vim.type_idx, vim.val_idx
+ nlua_init_types(lstate);
+ lua_setglobal(lstate, "vim");
+ return 0;
+}
+
+/// Initialize lua interpreter
+///
+/// Crashes NeoVim if initialization fails. Should be called once per lua
+/// interpreter instance.
+static lua_State *init_lua(void)
+ FUNC_ATTR_NONNULL_RET FUNC_ATTR_WARN_UNUSED_RESULT
+{
+ lua_State *lstate = luaL_newstate();
+ if (lstate == NULL) {
+ EMSG(_("E970: Failed to initialize lua interpreter"));
+ preserve_exit();
+ }
+ luaL_openlibs(lstate);
+ NLUA_CALL_C_FUNCTION_0(lstate, nlua_state_init, 0);
+ return lstate;
+}
+
+static lua_State *global_lstate = NULL;
+
+/// Execute lua string
+///
+/// @param[in] str String to execute.
+/// @param[out] ret_tv Location where result will be saved.
+///
+/// @return Result of the execution.
+void executor_exec_lua(const String str, typval_T *const ret_tv)
+ FUNC_ATTR_NONNULL_ALL
+{
+ if (global_lstate == NULL) {
+ global_lstate = init_lua();
+ }
+
+ NLUA_CALL_C_FUNCTION_2(global_lstate, nlua_exec_lua_string, 0,
+ (void *)&str, ret_tv);
+}
+
+/// Evaluate lua string
+///
+/// Used for luaeval(). Expects three values on the stack:
+///
+/// 1. String to evaluate.
+/// 2. _A value.
+/// 3. Pointer to location where result is saved.
+///
+/// @param[in,out] lstate Lua interpreter state.
+static int nlua_eval_lua_string(lua_State *const lstate)
+ FUNC_ATTR_NONNULL_ALL
+{
+ const String *const str = (const String *)lua_touserdata(lstate, 1);
+ typval_T *const arg = (typval_T *)lua_touserdata(lstate, 2);
+ typval_T *const ret_tv = (typval_T *)lua_touserdata(lstate, 3);
+ lua_pop(lstate, 3);
+
+ garray_T str_ga;
+ ga_init(&str_ga, 1, 80);
+#define EVALHEADER "local _A=select(1,...) return ("
+ const size_t lcmd_len = sizeof(EVALHEADER) - 1 + str->size + 1;
+ char *lcmd;
+ if (lcmd_len < IOSIZE) {
+ lcmd = (char *)IObuff;
+ } else {
+ lcmd = xmalloc(lcmd_len);
+ }
+ memcpy(lcmd, EVALHEADER, sizeof(EVALHEADER) - 1);
+ memcpy(lcmd + sizeof(EVALHEADER) - 1, str->data, str->size);
+ lcmd[lcmd_len - 1] = ')';
+#undef EVALHEADER
+ if (luaL_loadbuffer(lstate, lcmd, lcmd_len, NLUA_EVAL_NAME)) {
+ nlua_error(lstate,
+ _("E5107: Error while creating lua chunk for luaeval(): %.*s"));
+ if (lcmd != (char *)IObuff) {
+ xfree(lcmd);
+ }
+ return 0;
+ }
+ if (lcmd != (char *)IObuff) {
+ xfree(lcmd);
+ }
+
+ if (arg == NULL || arg->v_type == VAR_UNKNOWN) {
+ lua_pushnil(lstate);
+ } else {
+ nlua_push_typval(lstate, arg);
+ }
+ if (lua_pcall(lstate, 1, 1, 0)) {
+ nlua_error(lstate,
+ _("E5108: Error while calling lua chunk for luaeval(): %.*s"));
+ return 0;
+ }
+ if (!nlua_pop_typval(lstate, ret_tv)) {
+ return 0;
+ }
+
+ return 0;
+}
+
+/// Print as a Vim message
+///
+/// @param lstate Lua interpreter state.
+static int nlua_print(lua_State *const lstate)
+ FUNC_ATTR_NONNULL_ALL
+{
+#define PRINT_ERROR(msg) \
+ do { \
+ errmsg = msg; \
+ errmsg_len = sizeof(msg) - 1; \
+ goto nlua_print_error; \
+ } while (0)
+ const int nargs = lua_gettop(lstate);
+ lua_getglobal(lstate, "tostring");
+ const char *errmsg = NULL;
+ size_t errmsg_len = 0;
+ garray_T msg_ga;
+ ga_init(&msg_ga, 1, 80);
+ int curargidx = 1;
+ for (; curargidx <= nargs; curargidx++) {
+ lua_pushvalue(lstate, -1); // tostring
+ lua_pushvalue(lstate, curargidx); // arg
+ if (lua_pcall(lstate, 1, 1, 0)) {
+ errmsg = lua_tolstring(lstate, -1, &errmsg_len);
+ goto nlua_print_error;
+ }
+ size_t len;
+ const char *const s = lua_tolstring(lstate, -1, &len);
+ if (s == NULL) {
+ PRINT_ERROR(
+ "<Unknown error: lua_tolstring returned NULL for tostring result>");
+ }
+ ga_concat_len(&msg_ga, s, len);
+ if (curargidx < nargs) {
+ ga_append(&msg_ga, ' ');
+ }
+ lua_pop(lstate, 1);
+ }
+#undef PRINT_ERROR
+ lua_pop(lstate, nargs + 1);
+ ga_append(&msg_ga, NUL);
+ {
+ const size_t len = (size_t)msg_ga.ga_len - 1;
+ char *const str = (char *)msg_ga.ga_data;
+
+ for (size_t i = 0; i < len;) {
+ const size_t start = i;
+ while (i < len) {
+ switch (str[i]) {
+ case NUL: {
+ str[i] = NL;
+ i++;
+ continue;
+ }
+ case NL: {
+ str[i] = NUL;
+ i++;
+ break;
+ }
+ default: {
+ i++;
+ continue;
+ }
+ }
+ break;
+ }
+ msg((char_u *)str + start);
+ }
+ if (str[len - 1] == NUL) { // Last was newline
+ msg((char_u *)"");
+ }
+ }
+ ga_clear(&msg_ga);
+ return 0;
+nlua_print_error:
+ emsgf(_("E5114: Error while converting print argument #%i: %.*s"),
+ curargidx, errmsg_len, errmsg);
+ ga_clear(&msg_ga);
+ lua_pop(lstate, lua_gettop(lstate));
+ return 0;
+}
+
+/// debug.debug implementation: interaction with user while debugging
+///
+/// @param lstate Lua interpreter state.
+int nlua_debug(lua_State *lstate)
+ FUNC_ATTR_NONNULL_ALL
+{
+ const typval_T input_args[] = {
+ {
+ .v_lock = VAR_FIXED,
+ .v_type = VAR_STRING,
+ .vval.v_string = (char_u *)"lua_debug> ",
+ },
+ {
+ .v_type = VAR_UNKNOWN,
+ },
+ };
+ for (;;) {
+ lua_settop(lstate, 0);
+ typval_T input;
+ get_user_input(input_args, &input, false);
+ msg_putchar('\n'); // Avoid outputting on input line.
+ if (input.v_type != VAR_STRING
+ || input.vval.v_string == NULL
+ || *input.vval.v_string == NUL
+ || STRCMP(input.vval.v_string, "cont") == 0) {
+ tv_clear(&input);
+ return 0;
+ }
+ if (luaL_loadbuffer(lstate, (const char *)input.vval.v_string,
+ STRLEN(input.vval.v_string), "=(debug command)")) {
+ nlua_error(lstate, _("E5115: Error while loading debug string: %.*s"));
+ }
+ tv_clear(&input);
+ if (lua_pcall(lstate, 0, 0, 0)) {
+ nlua_error(lstate, _("E5116: Error while calling debug string: %.*s"));
+ }
+ }
+ return 0;
+}
+
+/// Evaluate lua string
+///
+/// Used for luaeval().
+///
+/// @param[in] str String to execute.
+/// @param[in] arg Second argument to `luaeval()`.
+/// @param[out] ret_tv Location where result will be saved.
+///
+/// @return Result of the execution.
+void executor_eval_lua(const String str, typval_T *const arg,
+ typval_T *const ret_tv)
+ FUNC_ATTR_NONNULL_ALL
+{
+ if (global_lstate == NULL) {
+ global_lstate = init_lua();
+ }
+
+ NLUA_CALL_C_FUNCTION_3(global_lstate, nlua_eval_lua_string, 0,
+ (void *)&str, arg, ret_tv);
+}
+
+/// Run lua string
+///
+/// Used for :lua.
+///
+/// @param eap VimL command being run.
+void ex_lua(exarg_T *const eap)
+ FUNC_ATTR_NONNULL_ALL
+{
+ size_t len;
+ char *const code = script_get(eap, &len);
+ if (eap->skip) {
+ xfree(code);
+ return;
+ }
+ typval_T tv = { .v_type = VAR_UNKNOWN };
+ executor_exec_lua((String) { .data = code, .size = len }, &tv);
+ tv_clear(&tv);
+ xfree(code);
+}
+
+/// Run lua string for each line in range
+///
+/// Used for :luado.
+///
+/// @param eap VimL command being run.
+void ex_luado(exarg_T *const eap)
+ FUNC_ATTR_NONNULL_ALL
+{
+ if (global_lstate == NULL) {
+ global_lstate = init_lua();
+ }
+ if (u_save(eap->line1 - 1, eap->line2 + 1) == FAIL) {
+ EMSG(_("cannot save undo information"));
+ return;
+ }
+ const String cmd = {
+ .size = STRLEN(eap->arg),
+ .data = (char *)eap->arg,
+ };
+ const linenr_T range[] = { eap->line1, eap->line2 };
+ NLUA_CALL_C_FUNCTION_2(global_lstate, nlua_exec_luado_string, 0,
+ (void *)&cmd, (void *)range);
+}
+
+/// Run lua file
+///
+/// Used for :luafile.
+///
+/// @param eap VimL command being run.
+void ex_luafile(exarg_T *const eap)
+ FUNC_ATTR_NONNULL_ALL
+{
+ if (global_lstate == NULL) {
+ global_lstate = init_lua();
+ }
+ NLUA_CALL_C_FUNCTION_1(global_lstate, nlua_exec_lua_file, 0,
+ (void *)eap->arg);
+}
diff --git a/src/nvim/lua/executor.h b/src/nvim/lua/executor.h
new file mode 100644
index 0000000000..0cbf290f64
--- /dev/null
+++ b/src/nvim/lua/executor.h
@@ -0,0 +1,25 @@
+#ifndef NVIM_LUA_EXECUTOR_H
+#define NVIM_LUA_EXECUTOR_H
+
+#include <lua.h>
+
+#include "nvim/api/private/defs.h"
+#include "nvim/func_attr.h"
+#include "nvim/eval/typval.h"
+#include "nvim/ex_cmds_defs.h"
+
+// Generated by msgpack-gen.lua
+void nlua_add_api_functions(lua_State *lstate) REAL_FATTR_NONNULL_ALL;
+
+#define set_api_error(s, err) \
+ do { \
+ Error *err_ = (err); \
+ err_->type = kErrorTypeException; \
+ err_->set = true; \
+ memcpy(&err_->msg[0], s, sizeof(s)); \
+ } while (0)
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "lua/executor.h.generated.h"
+#endif
+#endif // NVIM_LUA_EXECUTOR_H
diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua
new file mode 100644
index 0000000000..8d1c5bdf4f
--- /dev/null
+++ b/src/nvim/lua/vim.lua
@@ -0,0 +1,2 @@
+-- TODO(ZyX-I): Create compatibility layer.
+return {}
diff --git a/src/nvim/msgpack_rpc/helpers.c b/src/nvim/msgpack_rpc/helpers.c
index 91ef5524ea..444c6cc256 100644
--- a/src/nvim/msgpack_rpc/helpers.c
+++ b/src/nvim/msgpack_rpc/helpers.c
@@ -24,12 +24,12 @@ static msgpack_zone zone;
static msgpack_sbuffer sbuffer;
#define HANDLE_TYPE_CONVERSION_IMPL(t, lt) \
- bool msgpack_rpc_to_##lt(const msgpack_object *const obj, \
- Integer *const arg) \
- FUNC_ATTR_NONNULL_ALL \
+ static bool msgpack_rpc_to_##lt(const msgpack_object *const obj, \
+ Integer *const arg) \
+ FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT \
{ \
if (obj->type != MSGPACK_OBJECT_EXT \
- || obj->via.ext.type != kObjectType##t) { \
+ || obj->via.ext.type + EXT_OBJECT_TYPE_SHIFT != kObjectType##t) { \
return false; \
} \
\
@@ -48,13 +48,14 @@ static msgpack_sbuffer sbuffer;
return true; \
} \
\
- void msgpack_rpc_from_##lt(Integer o, msgpack_packer *res) \
+ static void msgpack_rpc_from_##lt(Integer o, msgpack_packer *res) \
FUNC_ATTR_NONNULL_ARG(2) \
{ \
msgpack_packer pac; \
msgpack_packer_init(&pac, &sbuffer, msgpack_sbuffer_write); \
msgpack_pack_int64(&pac, (handle_T)o); \
- msgpack_pack_ext(res, sbuffer.size, kObjectType##t); \
+ msgpack_pack_ext(res, sbuffer.size, \
+ kObjectType##t - EXT_OBJECT_TYPE_SHIFT); \
msgpack_pack_ext_body(res, sbuffer.data, sbuffer.size); \
msgpack_sbuffer_clear(&sbuffer); \
}
@@ -126,7 +127,7 @@ bool msgpack_rpc_to_object(const msgpack_object *const obj, Object *const arg)
{
STATIC_ASSERT(sizeof(Float) == sizeof(cur.mobj->via.f64),
"Msgpack floating-point size does not match API integer");
- *cur.aobj = FLOATING_OBJ(cur.mobj->via.f64);
+ *cur.aobj = FLOAT_OBJ(cur.mobj->via.f64);
break;
}
#define STR_CASE(type, attr, obj, dest, conv) \
@@ -225,7 +226,7 @@ bool msgpack_rpc_to_object(const msgpack_object *const obj, Object *const arg)
break;
}
case MSGPACK_OBJECT_EXT: {
- switch (cur.mobj->via.ext.type) {
+ switch ((ObjectType)(cur.mobj->via.ext.type + EXT_OBJECT_TYPE_SHIFT)) {
case kObjectTypeBuffer: {
cur.aobj->type = kObjectTypeBuffer;
ret = msgpack_rpc_to_buffer(cur.mobj, &cur.aobj->data.integer);
@@ -241,6 +242,15 @@ bool msgpack_rpc_to_object(const msgpack_object *const obj, Object *const arg)
ret = msgpack_rpc_to_tabpage(cur.mobj, &cur.aobj->data.integer);
break;
}
+ case kObjectTypeNil:
+ case kObjectTypeBoolean:
+ case kObjectTypeInteger:
+ case kObjectTypeFloat:
+ case kObjectTypeString:
+ case kObjectTypeArray:
+ case kObjectTypeDictionary: {
+ break;
+ }
}
break;
}
@@ -364,6 +374,9 @@ void msgpack_rpc_from_object(const Object result, msgpack_packer *const res)
kv_push(stack, ((APIToMPObjectStackItem) { &result, false, 0 }));
while (kv_size(stack)) {
APIToMPObjectStackItem cur = kv_last(stack);
+ STATIC_ASSERT(kObjectTypeWindow == kObjectTypeBuffer + 1
+ && kObjectTypeTabpage == kObjectTypeWindow + 1,
+ "Buffer, window and tabpage enum items are in order");
switch (cur.aobj->type) {
case kObjectTypeNil: {
msgpack_pack_nil(res);
diff --git a/src/nvim/msgpack_rpc/helpers.h b/src/nvim/msgpack_rpc/helpers.h
index 7d9f114140..0e4cd1be6d 100644
--- a/src/nvim/msgpack_rpc/helpers.h
+++ b/src/nvim/msgpack_rpc/helpers.h
@@ -9,6 +9,13 @@
#include "nvim/event/wstream.h"
#include "nvim/api/private/defs.h"
+/// Value by which objects represented as EXT type are shifted
+///
+/// Subtracted when packing, added when unpacking. Used to allow moving
+/// buffer/window/tabpage block inside ObjectType enum. This block yet cannot be
+/// split or reordered.
+#define EXT_OBJECT_TYPE_SHIFT kObjectTypeBuffer
+
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "msgpack_rpc/helpers.h.generated.h"
#endif
diff --git a/test/functional/api/buffer_spec.lua b/test/functional/api/buffer_spec.lua
index 9699ea8f85..823c134ebd 100644
--- a/test/functional/api/buffer_spec.lua
+++ b/test/functional/api/buffer_spec.lua
@@ -10,6 +10,7 @@ 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)
@@ -121,6 +122,15 @@ describe('api/buf', 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'})
diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua
index 7c79d8832f..282ecbfd87 100644
--- a/test/functional/api/vim_spec.lua
+++ b/test/functional/api/vim_spec.lua
@@ -337,6 +337,17 @@ describe('api', function()
eq('\128\253\44', helpers.nvim('replace_termcodes',
'<LeftMouse>', true, true, true))
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('nvim_feedkeys', function()
diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua
index 6882f50a3e..8a65d3f71e 100644
--- a/test/functional/api/window_spec.lua
+++ b/test/functional/api/window_spec.lua
@@ -9,6 +9,7 @@ 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
-- check if str is visible at the beginning of some line
@@ -55,6 +56,12 @@ describe('api/win', 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>')
diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua
index 2919165280..1be70f917c 100644
--- a/test/functional/helpers.lua
+++ b/test/functional/helpers.lua
@@ -566,7 +566,7 @@ local function get_pathsep()
return funcs.fnamemodify('.', ':p'):sub(-1)
end
-local M = {
+local module = {
prepend_argv = prepend_argv,
clear = clear,
connect = connect,
@@ -641,5 +641,5 @@ return function(after_each)
check_cores('build/bin/nvim')
end)
end
- return M
+ return module
end
diff --git a/test/functional/lua/api_spec.lua b/test/functional/lua/api_spec.lua
new file mode 100644
index 0000000000..b1dc5c07fd
--- /dev/null
+++ b/test/functional/lua/api_spec.lua
@@ -0,0 +1,204 @@
+-- Test suite for testing interactions with API bindings
+local helpers = require('test.functional.helpers')(after_each)
+
+local exc_exec = helpers.exc_exec
+local funcs = helpers.funcs
+local clear = helpers.clear
+local eval = helpers.eval
+local NIL = helpers.NIL
+local eq = helpers.eq
+
+before_each(clear)
+
+describe('luaeval(vim.api.…)', function()
+ describe('with channel_id and buffer handle', function()
+ describe('nvim_buf_get_lines', function()
+ it('works', function()
+ funcs.setline(1, {"abc", "def", "a\nb", "ttt"})
+ eq({{_TYPE={}, _VAL={'a\nb'}}},
+ funcs.luaeval('vim.api.nvim_buf_get_lines(1, 2, 3, false)'))
+ end)
+ end)
+ describe('nvim_buf_set_lines', function()
+ it('works', function()
+ funcs.setline(1, {"abc", "def", "a\nb", "ttt"})
+ eq(NIL, funcs.luaeval('vim.api.nvim_buf_set_lines(1, 1, 2, false, {"b\\0a"})'))
+ eq({'abc', {_TYPE={}, _VAL={'b\na'}}, {_TYPE={}, _VAL={'a\nb'}}, 'ttt'},
+ funcs.luaeval('vim.api.nvim_buf_get_lines(1, 0, 4, false)'))
+ end)
+ end)
+ end)
+ describe('with errors', function()
+ it('transforms API error from nvim_buf_set_lines into lua error', function()
+ funcs.setline(1, {"abc", "def", "a\nb", "ttt"})
+ eq({false, 'String cannot contain newlines'},
+ funcs.luaeval('{pcall(vim.api.nvim_buf_set_lines, 1, 1, 2, false, {"b\\na"})}'))
+ end)
+
+ it('transforms API error from nvim_win_set_cursor into lua error', function()
+ eq({false, 'Argument "pos" must be a [row, col] array'},
+ funcs.luaeval('{pcall(vim.api.nvim_win_set_cursor, 0, {1, 2, 3})}'))
+ -- Used to produce a memory leak due to a bug in nvim_win_set_cursor
+ eq({false, 'Invalid window id'},
+ funcs.luaeval('{pcall(vim.api.nvim_win_set_cursor, -1, {1, 2, 3})}'))
+ end)
+
+ it('transforms API error from nvim_win_set_cursor + same array as in first test into lua error',
+ function()
+ eq({false, 'Argument "pos" must be a [row, col] array'},
+ funcs.luaeval('{pcall(vim.api.nvim_win_set_cursor, 0, {"b\\na"})}'))
+ end)
+ end)
+
+ it('correctly evaluates API code which calls luaeval', function()
+ local str = (([===[vim.api.nvim_eval([==[
+ luaeval('vim.api.nvim_eval([=[
+ luaeval("vim.api.nvim_eval([[
+ luaeval(1)
+ ]])")
+ ]=])')
+ ]==])]===]):gsub('\n', ' '))
+ eq(1, funcs.luaeval(str))
+ end)
+
+ it('correctly converts from API objects', function()
+ eq(1, funcs.luaeval('vim.api.nvim_eval("1")'))
+ eq('1', funcs.luaeval([[vim.api.nvim_eval('"1"')]]))
+ eq({}, funcs.luaeval('vim.api.nvim_eval("[]")'))
+ eq({}, funcs.luaeval('vim.api.nvim_eval("{}")'))
+ eq(1, funcs.luaeval('vim.api.nvim_eval("1.0")'))
+ eq(true, funcs.luaeval('vim.api.nvim_eval("v:true")'))
+ eq(false, funcs.luaeval('vim.api.nvim_eval("v:false")'))
+ eq(NIL, funcs.luaeval('vim.api.nvim_eval("v:null")'))
+
+ eq(0, eval([[type(luaeval('vim.api.nvim_eval("1")'))]]))
+ eq(1, eval([[type(luaeval('vim.api.nvim_eval("''1''")'))]]))
+ eq(3, eval([[type(luaeval('vim.api.nvim_eval("[]")'))]]))
+ eq(4, eval([[type(luaeval('vim.api.nvim_eval("{}")'))]]))
+ eq(5, eval([[type(luaeval('vim.api.nvim_eval("1.0")'))]]))
+ eq(6, eval([[type(luaeval('vim.api.nvim_eval("v:true")'))]]))
+ eq(6, eval([[type(luaeval('vim.api.nvim_eval("v:false")'))]]))
+ eq(7, eval([[type(luaeval('vim.api.nvim_eval("v:null")'))]]))
+
+ eq({foo=42}, funcs.luaeval([[vim.api.nvim_eval('{"foo": 42}')]]))
+ eq({42}, funcs.luaeval([[vim.api.nvim_eval('[42]')]]))
+
+ eq({foo={bar=42}, baz=50}, funcs.luaeval([[vim.api.nvim_eval('{"foo": {"bar": 42}, "baz": 50}')]]))
+ eq({{42}, {}}, funcs.luaeval([=[vim.api.nvim_eval('[[42], []]')]=]))
+ end)
+
+ it('correctly converts to API objects', function()
+ eq(1, funcs.luaeval('vim.api.nvim__id(1)'))
+ eq('1', funcs.luaeval('vim.api.nvim__id("1")'))
+ eq({1}, funcs.luaeval('vim.api.nvim__id({1})'))
+ eq({foo=1}, funcs.luaeval('vim.api.nvim__id({foo=1})'))
+ eq(1.5, funcs.luaeval('vim.api.nvim__id(1.5)'))
+ eq(true, funcs.luaeval('vim.api.nvim__id(true)'))
+ eq(false, funcs.luaeval('vim.api.nvim__id(false)'))
+ eq(NIL, funcs.luaeval('vim.api.nvim__id(nil)'))
+
+ eq(0, eval([[type(luaeval('vim.api.nvim__id(1)'))]]))
+ eq(1, eval([[type(luaeval('vim.api.nvim__id("1")'))]]))
+ eq(3, eval([[type(luaeval('vim.api.nvim__id({1})'))]]))
+ eq(4, eval([[type(luaeval('vim.api.nvim__id({foo=1})'))]]))
+ eq(5, eval([[type(luaeval('vim.api.nvim__id(1.5)'))]]))
+ eq(6, eval([[type(luaeval('vim.api.nvim__id(true)'))]]))
+ eq(6, eval([[type(luaeval('vim.api.nvim__id(false)'))]]))
+ eq(7, eval([[type(luaeval('vim.api.nvim__id(nil)'))]]))
+
+ eq({foo=1, bar={42, {{baz=true}, 5}}}, funcs.luaeval('vim.api.nvim__id({foo=1, bar={42, {{baz=true}, 5}}})'))
+ end)
+
+ it('correctly converts container objects with type_idx to API objects', function()
+ eq(5, eval('type(luaeval("vim.api.nvim__id({[vim.type_idx]=vim.types.float, [vim.val_idx]=0})"))'))
+ eq(4, eval([[type(luaeval('vim.api.nvim__id({[vim.type_idx]=vim.types.dictionary})'))]]))
+ eq(3, eval([[type(luaeval('vim.api.nvim__id({[vim.type_idx]=vim.types.array})'))]]))
+
+ eq({}, funcs.luaeval('vim.api.nvim__id({[vim.type_idx]=vim.types.array})'))
+
+ -- Presence of type_idx makes Vim ignore some keys
+ eq({42}, funcs.luaeval('vim.api.nvim__id({[vim.type_idx]=vim.types.array, [vim.val_idx]=10, [5]=1, foo=2, [1]=42})'))
+ eq({foo=2}, funcs.luaeval('vim.api.nvim__id({[vim.type_idx]=vim.types.dictionary, [vim.val_idx]=10, [5]=1, foo=2, [1]=42})'))
+ eq(10, funcs.luaeval('vim.api.nvim__id({[vim.type_idx]=vim.types.float, [vim.val_idx]=10, [5]=1, foo=2, [1]=42})'))
+ eq({}, funcs.luaeval('vim.api.nvim__id({[vim.type_idx]=vim.types.array, [vim.val_idx]=10, [5]=1, foo=2})'))
+ end)
+
+ it('correctly converts arrays with type_idx to API objects', function()
+ eq(3, eval([[type(luaeval('vim.api.nvim__id_array({[vim.type_idx]=vim.types.array})'))]]))
+
+ eq({}, funcs.luaeval('vim.api.nvim__id_array({[vim.type_idx]=vim.types.array})'))
+
+ eq({42}, funcs.luaeval('vim.api.nvim__id_array({[vim.type_idx]=vim.types.array, [vim.val_idx]=10, [5]=1, foo=2, [1]=42})'))
+ eq({{foo=2}}, funcs.luaeval('vim.api.nvim__id_array({{[vim.type_idx]=vim.types.dictionary, [vim.val_idx]=10, [5]=1, foo=2, [1]=42}})'))
+ eq({10}, funcs.luaeval('vim.api.nvim__id_array({{[vim.type_idx]=vim.types.float, [vim.val_idx]=10, [5]=1, foo=2, [1]=42}})'))
+ eq({}, funcs.luaeval('vim.api.nvim__id_array({[vim.type_idx]=vim.types.array, [vim.val_idx]=10, [5]=1, foo=2})'))
+
+ eq({}, funcs.luaeval('vim.api.nvim__id_array({})'))
+ eq(3, eval([[type(luaeval('vim.api.nvim__id_array({})'))]]))
+ end)
+
+ it('correctly converts dictionaries with type_idx to API objects', function()
+ eq(4, eval([[type(luaeval('vim.api.nvim__id_dictionary({[vim.type_idx]=vim.types.dictionary})'))]]))
+
+ eq({}, funcs.luaeval('vim.api.nvim__id_dictionary({[vim.type_idx]=vim.types.dictionary})'))
+
+ eq({v={42}}, funcs.luaeval('vim.api.nvim__id_dictionary({v={[vim.type_idx]=vim.types.array, [vim.val_idx]=10, [5]=1, foo=2, [1]=42}})'))
+ eq({foo=2}, funcs.luaeval('vim.api.nvim__id_dictionary({[vim.type_idx]=vim.types.dictionary, [vim.val_idx]=10, [5]=1, foo=2, [1]=42})'))
+ eq({v=10}, funcs.luaeval('vim.api.nvim__id_dictionary({v={[vim.type_idx]=vim.types.float, [vim.val_idx]=10, [5]=1, foo=2, [1]=42}})'))
+ eq({v={}}, funcs.luaeval('vim.api.nvim__id_dictionary({v={[vim.type_idx]=vim.types.array, [vim.val_idx]=10, [5]=1, foo=2}})'))
+
+ -- If API requests dictionary, then empty table will be the one. This is not
+ -- the case normally because empty table is an empty arrray.
+ eq({}, funcs.luaeval('vim.api.nvim__id_dictionary({})'))
+ eq(4, eval([[type(luaeval('vim.api.nvim__id_dictionary({})'))]]))
+ end)
+
+ it('errors out correctly when working with API', function()
+ -- Conversion errors
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Cannot convert given lua type',
+ exc_exec([[call luaeval("vim.api.nvim__id(vim.api.nvim__id)")]]))
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Cannot convert given lua table',
+ exc_exec([[call luaeval("vim.api.nvim__id({1, foo=42})")]]))
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Cannot convert given lua type',
+ exc_exec([[call luaeval("vim.api.nvim__id({42, vim.api.nvim__id})")]]))
+ -- Errors in number of arguments
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected 1 argument',
+ exc_exec([[call luaeval("vim.api.nvim__id()")]]))
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected 1 argument',
+ exc_exec([[call luaeval("vim.api.nvim__id(1, 2)")]]))
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected 2 arguments',
+ exc_exec([[call luaeval("vim.api.nvim_set_var(1, 2, 3)")]]))
+ -- Error in argument types
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected lua string',
+ exc_exec([[call luaeval("vim.api.nvim_set_var(1, 2)")]]))
+
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected lua number',
+ exc_exec([[call luaeval("vim.api.nvim_buf_get_lines(0, 'test', 1, false)")]]))
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Number is not integral',
+ exc_exec([[call luaeval("vim.api.nvim_buf_get_lines(0, 1.5, 1, false)")]]))
+
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected lua table',
+ exc_exec([[call luaeval("vim.api.nvim__id_float('test')")]]))
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Unexpected type',
+ exc_exec([[call luaeval("vim.api.nvim__id_float({[vim.type_idx]=vim.types.dictionary})")]]))
+
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected lua table',
+ exc_exec([[call luaeval("vim.api.nvim__id_array(1)")]]))
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Unexpected type',
+ exc_exec([[call luaeval("vim.api.nvim__id_array({[vim.type_idx]=vim.types.dictionary})")]]))
+
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Expected lua table',
+ exc_exec([[call luaeval("vim.api.nvim__id_dictionary(1)")]]))
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: Unexpected type',
+ exc_exec([[call luaeval("vim.api.nvim__id_dictionary({[vim.type_idx]=vim.types.array})")]]))
+ -- TODO: check for errors with Tabpage argument
+ -- TODO: check for errors with Window argument
+ -- TODO: check for errors with Buffer argument
+ end)
+
+ it('accepts any value as API Boolean', function()
+ eq('', funcs.luaeval('vim.api.nvim_replace_termcodes("", vim, false, nil)'))
+ eq('', funcs.luaeval('vim.api.nvim_replace_termcodes("", 0, 1.5, "test")'))
+ eq('', funcs.luaeval('vim.api.nvim_replace_termcodes("", true, {}, {[vim.type_idx]=vim.types.array})'))
+ end)
+end)
diff --git a/test/functional/lua/commands_spec.lua b/test/functional/lua/commands_spec.lua
new file mode 100644
index 0000000000..017ee55729
--- /dev/null
+++ b/test/functional/lua/commands_spec.lua
@@ -0,0 +1,164 @@
+-- Test suite for checking :lua* commands
+local helpers = require('test.functional.helpers')(after_each)
+
+local eq = helpers.eq
+local NIL = helpers.NIL
+local clear = helpers.clear
+local meths = helpers.meths
+local funcs = helpers.funcs
+local source = helpers.source
+local dedent = helpers.dedent
+local exc_exec = helpers.exc_exec
+local write_file = helpers.write_file
+local redir_exec = helpers.redir_exec
+local curbufmeths = helpers.curbufmeths
+
+before_each(clear)
+
+describe(':lua command', function()
+ it('works', function()
+ eq('', redir_exec(
+ 'lua vim.api.nvim_buf_set_lines(1, 1, 2, false, {"TEST"})'))
+ eq({'', 'TEST'}, curbufmeths.get_lines(0, 100, false))
+ source(dedent([[
+ lua << EOF
+ vim.api.nvim_buf_set_lines(1, 1, 2, false, {"TSET"})
+ EOF]]))
+ eq({'', 'TSET'}, curbufmeths.get_lines(0, 100, false))
+ source(dedent([[
+ lua << EOF
+ vim.api.nvim_buf_set_lines(1, 1, 2, false, {"SETT"})]]))
+ eq({'', 'SETT'}, curbufmeths.get_lines(0, 100, false))
+ source(dedent([[
+ lua << EOF
+ vim.api.nvim_buf_set_lines(1, 1, 2, false, {"ETTS"})
+ vim.api.nvim_buf_set_lines(1, 2, 3, false, {"TTSE"})
+ vim.api.nvim_buf_set_lines(1, 3, 4, false, {"STTE"})
+ EOF]]))
+ eq({'', 'ETTS', 'TTSE', 'STTE'}, curbufmeths.get_lines(0, 100, false))
+ end)
+ it('throws catchable errors', function()
+ eq([[Vim(lua):E5104: Error while creating lua chunk: [string "<VimL compiled string>"]:1: unexpected symbol near ')']],
+ exc_exec('lua ()'))
+ eq([[Vim(lua):E5105: Error while calling lua chunk: [string "<VimL compiled string>"]:1: TEST]],
+ exc_exec('lua error("TEST")'))
+ eq([[Vim(lua):E5105: Error while calling lua chunk: [string "<VimL compiled string>"]:1: Invalid buffer id]],
+ exc_exec('lua vim.api.nvim_buf_set_lines(-10, 1, 1, false, {"TEST"})'))
+ eq({''}, curbufmeths.get_lines(0, 100, false))
+ end)
+ it('works with NULL errors', function()
+ eq([=[Vim(lua):E5105: Error while calling lua chunk: [NULL]]=],
+ exc_exec('lua error(nil)'))
+ end)
+ it('accepts embedded NLs without heredoc', function()
+ -- Such code is usually used for `:execute 'lua' {generated_string}`:
+ -- heredocs do not work in this case.
+ meths.command([[
+ lua
+ vim.api.nvim_buf_set_lines(1, 1, 2, false, {"ETTS"})
+ vim.api.nvim_buf_set_lines(1, 2, 3, false, {"TTSE"})
+ vim.api.nvim_buf_set_lines(1, 3, 4, false, {"STTE"})
+ ]])
+ eq({'', 'ETTS', 'TTSE', 'STTE'}, curbufmeths.get_lines(0, 100, false))
+ end)
+ it('preserves global and not preserves local variables', function()
+ eq('', redir_exec('lua gvar = 42'))
+ eq('', redir_exec('lua local lvar = 100500'))
+ eq(NIL, funcs.luaeval('lvar'))
+ eq(42, funcs.luaeval('gvar'))
+ end)
+ it('works with long strings', function()
+ local s = ('x'):rep(100500)
+
+ eq('\nE5104: Error while creating lua chunk: [string "<VimL compiled string>"]:1: unfinished string near \'<eof>\'', redir_exec(('lua vim.api.nvim_buf_set_lines(1, 1, 2, false, {"%s})'):format(s)))
+ eq({''}, curbufmeths.get_lines(0, -1, false))
+
+ eq('', redir_exec(('lua vim.api.nvim_buf_set_lines(1, 1, 2, false, {"%s"})'):format(s)))
+ eq({'', s}, curbufmeths.get_lines(0, -1, false))
+ end)
+end)
+
+describe(':luado command', function()
+ it('works', function()
+ curbufmeths.set_lines(0, 1, false, {"ABC", "def", "gHi"})
+ eq('', redir_exec('luado lines = (lines or {}) lines[#lines + 1] = {linenr, line}'))
+ eq({'ABC', 'def', 'gHi'}, curbufmeths.get_lines(0, -1, false))
+ eq({{1, 'ABC'}, {2, 'def'}, {3, 'gHi'}}, funcs.luaeval('lines'))
+
+ -- Automatic transformation of numbers
+ eq('', redir_exec('luado return linenr'))
+ eq({'1', '2', '3'}, curbufmeths.get_lines(0, -1, false))
+
+ eq('', redir_exec('luado return ("<%02x>"):format(line:byte())'))
+ eq({'<31>', '<32>', '<33>'}, curbufmeths.get_lines(0, -1, false))
+ end)
+ it('stops processing lines when suddenly out of lines', function()
+ curbufmeths.set_lines(0, 1, false, {"ABC", "def", "gHi"})
+ eq('', redir_exec('2,$luado runs = ((runs or 0) + 1) vim.api.nvim_command("%d")'))
+ eq({''}, curbufmeths.get_lines(0, -1, false))
+ eq(1, funcs.luaeval('runs'))
+ end)
+ it('works correctly when changing lines out of range', function()
+ curbufmeths.set_lines(0, 1, false, {"ABC", "def", "gHi"})
+ eq('\nE322: line number out of range: 1 past the end\nE320: Cannot find line 2',
+ redir_exec('2,$luado vim.api.nvim_command("%d") return linenr'))
+ eq({''}, curbufmeths.get_lines(0, -1, false))
+ end)
+ it('fails on errors', function()
+ eq([[Vim(luado):E5109: Error while creating lua chunk: [string "<VimL compiled string>"]:1: unexpected symbol near ')']],
+ exc_exec('luado ()'))
+ eq([[Vim(luado):E5111: Error while calling lua function: [string "<VimL compiled string>"]:1: attempt to perform arithmetic on global 'liness' (a nil value)]],
+ exc_exec('luado return liness + 1'))
+ end)
+ it('works with NULL errors', function()
+ eq([=[Vim(luado):E5111: Error while calling lua function: [NULL]]=],
+ exc_exec('luado error(nil)'))
+ end)
+ it('fails in sandbox when needed', function()
+ curbufmeths.set_lines(0, 1, false, {"ABC", "def", "gHi"})
+ eq('\nE48: Not allowed in sandbox: sandbox luado runs = (runs or 0) + 1',
+ redir_exec('sandbox luado runs = (runs or 0) + 1'))
+ eq(NIL, funcs.luaeval('runs'))
+ end)
+ it('works with long strings', function()
+ local s = ('x'):rep(100500)
+
+ eq('\nE5109: Error while creating lua chunk: [string "<VimL compiled string>"]:1: unfinished string near \'<eof>\'', redir_exec(('luado return "%s'):format(s)))
+ eq({''}, curbufmeths.get_lines(0, -1, false))
+
+ eq('', redir_exec(('luado return "%s"'):format(s)))
+ eq({s}, curbufmeths.get_lines(0, -1, false))
+ end)
+end)
+
+describe(':luafile', function()
+ local fname = 'Xtest-functional-lua-commands-luafile'
+
+ after_each(function()
+ os.remove(fname)
+ end)
+
+ it('works', function()
+ write_file(fname, [[
+ vim.api.nvim_buf_set_lines(1, 1, 2, false, {"ETTS"})
+ vim.api.nvim_buf_set_lines(1, 2, 3, false, {"TTSE"})
+ vim.api.nvim_buf_set_lines(1, 3, 4, false, {"STTE"})
+ ]])
+ eq('', redir_exec('luafile ' .. fname))
+ eq({'', 'ETTS', 'TTSE', 'STTE'}, curbufmeths.get_lines(0, 100, false))
+ end)
+
+ it('correctly errors out', function()
+ write_file(fname, '()')
+ eq(("Vim(luafile):E5112: Error while creating lua chunk: %s:1: unexpected symbol near ')'"):format(fname),
+ exc_exec('luafile ' .. fname))
+ write_file(fname, 'vimm.api.nvim_buf_set_lines(1, 1, 2, false, {"ETTS"})')
+ eq(("Vim(luafile):E5113: Error while calling lua chunk: %s:1: attempt to index global 'vimm' (a nil value)"):format(fname),
+ exc_exec('luafile ' .. fname))
+ end)
+ it('works with NULL errors', function()
+ write_file(fname, 'error(nil)')
+ eq([=[Vim(luafile):E5113: Error while calling lua chunk: [NULL]]=],
+ exc_exec('luafile ' .. fname))
+ end)
+end)
diff --git a/test/functional/lua/luaeval_spec.lua b/test/functional/lua/luaeval_spec.lua
new file mode 100644
index 0000000000..6da0001cea
--- /dev/null
+++ b/test/functional/lua/luaeval_spec.lua
@@ -0,0 +1,255 @@
+-- Test suite for testing luaeval() function
+local helpers = require('test.functional.helpers')(after_each)
+
+local redir_exec = helpers.redir_exec
+local exc_exec = helpers.exc_exec
+local command = helpers.command
+local meths = helpers.meths
+local funcs = helpers.funcs
+local clear = helpers.clear
+local eval = helpers.eval
+local NIL = helpers.NIL
+local eq = helpers.eq
+
+before_each(clear)
+
+local function startswith(expected, actual)
+ eq(expected, actual:sub(1, #expected))
+end
+
+describe('luaeval()', function()
+ local nested_by_level = {}
+ local nested = {}
+ local nested_s = '{}'
+ for i=1,100 do
+ if i % 2 == 0 then
+ nested = {nested}
+ nested_s = '{' .. nested_s .. '}'
+ else
+ nested = {nested=nested}
+ nested_s = '{nested=' .. nested_s .. '}'
+ end
+ nested_by_level[i] = {o=nested, s=nested_s}
+ end
+
+ describe('second argument', function()
+ it('is successfully received', function()
+ local t = {t=true, f=false, --[[n=NIL,]] d={l={'string', 42, 0.42}}}
+ eq(t, funcs.luaeval("_A", t))
+ -- Not tested: nil, funcrefs, returned object identity: behaviour will
+ -- most likely change.
+ end)
+ end)
+ describe('lua values', function()
+ it('are successfully transformed', function()
+ eq({n=1, f=1.5, s='string', l={4, 2}},
+ funcs.luaeval('{n=1, f=1.5, s="string", l={4, 2}}'))
+ -- Not tested: nil inside containers: behaviour will most likely change.
+ eq(NIL, funcs.luaeval('nil'))
+ eq({['']=1}, funcs.luaeval('{[""]=1}'))
+ end)
+ end)
+ describe('recursive lua values', function()
+ it('are successfully transformed', function()
+ funcs.luaeval('rawset(_G, "d", {})')
+ funcs.luaeval('rawset(d, "d", d)')
+ eq('\n{\'d\': {...@0}}', funcs.execute('echo luaeval("d")'))
+
+ funcs.luaeval('rawset(_G, "l", {})')
+ funcs.luaeval('table.insert(l, l)')
+ eq('\n[[...@0]]', funcs.execute('echo luaeval("l")'))
+ end)
+ end)
+ describe('strings', function()
+ it('are successfully converted to special dictionaries', function()
+ command([[let s = luaeval('"\0"')]])
+ eq({_TYPE={}, _VAL={'\n'}}, meths.get_var('s'))
+ eq(1, funcs.eval('s._TYPE is v:msgpack_types.binary'))
+ end)
+ it('are successfully converted to special dictionaries in table keys',
+ function()
+ command([[let d = luaeval('{["\0"]=1}')]])
+ eq({_TYPE={}, _VAL={{{_TYPE={}, _VAL={'\n'}}, 1}}}, meths.get_var('d'))
+ eq(1, funcs.eval('d._TYPE is v:msgpack_types.map'))
+ eq(1, funcs.eval('d._VAL[0][0]._TYPE is v:msgpack_types.string'))
+ end)
+ it('are successfully converted to special dictionaries from a list',
+ function()
+ command([[let l = luaeval('{"abc", "a\0b", "c\0d", "def"}')]])
+ eq({'abc', {_TYPE={}, _VAL={'a\nb'}}, {_TYPE={}, _VAL={'c\nd'}}, 'def'},
+ meths.get_var('l'))
+ eq(1, funcs.eval('l[1]._TYPE is v:msgpack_types.binary'))
+ eq(1, funcs.eval('l[2]._TYPE is v:msgpack_types.binary'))
+ end)
+ end)
+
+ -- Not checked: funcrefs converted to NIL. To be altered to something more
+ -- meaningful later.
+
+ it('correctly evaluates scalars', function()
+ eq(1, funcs.luaeval('1'))
+ eq(0, eval('type(luaeval("1"))'))
+
+ eq(1.5, funcs.luaeval('1.5'))
+ eq(5, eval('type(luaeval("1.5"))'))
+
+ eq("test", funcs.luaeval('"test"'))
+ eq(1, eval('type(luaeval("\'test\'"))'))
+
+ eq('', funcs.luaeval('""'))
+ eq({_TYPE={}, _VAL={'\n'}}, funcs.luaeval([['\0']]))
+ eq({_TYPE={}, _VAL={'\n', '\n'}}, funcs.luaeval([['\0\n\0']]))
+ eq(1, eval([[luaeval('"\0\n\0"')._TYPE is v:msgpack_types.binary]]))
+
+ eq(true, funcs.luaeval('true'))
+ eq(false, funcs.luaeval('false'))
+ eq(NIL, funcs.luaeval('nil'))
+ end)
+
+ it('correctly evaluates containers', function()
+ eq({}, funcs.luaeval('{}'))
+ eq(3, eval('type(luaeval("{}"))'))
+
+ eq({test=1, foo=2}, funcs.luaeval('{test=1, foo=2}'))
+ eq(4, eval('type(luaeval("{test=1, foo=2}"))'))
+
+ eq({4, 2}, funcs.luaeval('{4, 2}'))
+ eq(3, eval('type(luaeval("{4, 2}"))'))
+
+ local level = 30
+ eq(nested_by_level[level].o, funcs.luaeval(nested_by_level[level].s))
+
+ eq({_TYPE={}, _VAL={{{_TYPE={}, _VAL={'\n', '\n'}}, {_TYPE={}, _VAL={'\n', '\n\n'}}}}},
+ funcs.luaeval([[{['\0\n\0']='\0\n\0\0'}]]))
+ eq(1, eval([[luaeval('{["\0\n\0"]="\0\n\0\0"}')._TYPE is v:msgpack_types.map]]))
+ eq(1, eval([[luaeval('{["\0\n\0"]="\0\n\0\0"}')._VAL[0][0]._TYPE is v:msgpack_types.string]]))
+ eq(1, eval([[luaeval('{["\0\n\0"]="\0\n\0\0"}')._VAL[0][1]._TYPE is v:msgpack_types.binary]]))
+ eq({nested={{_TYPE={}, _VAL={{{_TYPE={}, _VAL={'\n', '\n'}}, {_TYPE={}, _VAL={'\n', '\n\n'}}}}}}},
+ funcs.luaeval([[{nested={{['\0\n\0']='\0\n\0\0'}}}]]))
+ end)
+
+ it('correctly passes scalars as argument', function()
+ eq(1, funcs.luaeval('_A', 1))
+ eq(1.5, funcs.luaeval('_A', 1.5))
+ eq('', funcs.luaeval('_A', ''))
+ eq('test', funcs.luaeval('_A', 'test'))
+ eq(NIL, funcs.luaeval('_A', NIL))
+ eq(true, funcs.luaeval('_A', true))
+ eq(false, funcs.luaeval('_A', false))
+ end)
+
+ it('correctly passes containers as argument', function()
+ eq({}, funcs.luaeval('_A', {}))
+ eq({test=1}, funcs.luaeval('_A', {test=1}))
+ eq({4, 2}, funcs.luaeval('_A', {4, 2}))
+ local level = 28
+ eq(nested_by_level[level].o, funcs.luaeval('_A', nested_by_level[level].o))
+ end)
+
+ local function sp(typ, val)
+ return ('{"_TYPE": v:msgpack_types.%s, "_VAL": %s}'):format(typ, val)
+ end
+ local function mapsp(...)
+ local val = ''
+ for i=1,(select('#', ...)/2) do
+ val = ('%s[%s,%s],'):format(val, select(i * 2 - 1, ...),
+ select(i * 2, ...))
+ end
+ return sp('map', '[' .. val .. ']')
+ end
+ local function luaevalarg(argexpr, expr)
+ return eval(([=[
+ [
+ extend(g:, {'_ret': luaeval(%s, %s)})._ret,
+ type(g:_ret)==type({})&&has_key(g:_ret, '_TYPE')
+ ? [
+ get(keys(filter(copy(v:msgpack_types), 'v:val is g:_ret._TYPE')), 0,
+ g:_ret._TYPE),
+ get(g:_ret, '_VAL', g:_ret)
+ ]
+ : [0, g:_ret]][1]
+ ]=]):format(expr or '"_A"', argexpr):gsub('\n', ''))
+ end
+
+ it('correctly passes special dictionaries', function()
+ eq({'binary', {'\n', '\n'}}, luaevalarg(sp('binary', '["\\n", "\\n"]')))
+ eq({'binary', {'\n', '\n'}}, luaevalarg(sp('string', '["\\n", "\\n"]')))
+ eq({0, true}, luaevalarg(sp('boolean', 1)))
+ eq({0, false}, luaevalarg(sp('boolean', 0)))
+ eq({0, NIL}, luaevalarg(sp('nil', 0)))
+ eq({0, {[""]=""}}, luaevalarg(mapsp(sp('binary', '[""]'), '""')))
+ eq({0, {[""]=""}}, luaevalarg(mapsp(sp('string', '[""]'), '""')))
+ end)
+
+ it('issues an error in some cases', function()
+ eq("Vim(call):E5100: Cannot convert given lua table: table should either have a sequence of positive integer keys or contain only string keys",
+ exc_exec('call luaeval("{1, foo=2}")'))
+ eq("Vim(call):E5101: Cannot convert given lua type",
+ exc_exec('call luaeval("vim.api.nvim_buf_get_lines")'))
+ startswith("Vim(call):E5107: Error while creating lua chunk for luaeval(): ",
+ exc_exec('call luaeval("1, 2, 3")'))
+ startswith("Vim(call):E5108: Error while calling lua chunk for luaeval(): ",
+ exc_exec('call luaeval("(nil)()")'))
+ eq("Vim(call):E5101: Cannot convert given lua type",
+ exc_exec('call luaeval("{42, vim.api}")'))
+ eq("Vim(call):E5101: Cannot convert given lua type",
+ exc_exec('call luaeval("{foo=42, baz=vim.api}")'))
+
+ -- The following should not crash: conversion error happens inside
+ eq("Vim(call):E5101: Cannot convert given lua type",
+ exc_exec('call luaeval("vim.api")'))
+ -- The following should not show internal error
+ eq("\nE5101: Cannot convert given lua type\n0",
+ redir_exec('echo luaeval("vim.api")'))
+ end)
+
+ it('correctly converts containers with type_idx', function()
+ eq(5, eval('type(luaeval("{[vim.type_idx]=vim.types.float, [vim.val_idx]=0}"))'))
+ eq(4, eval([[type(luaeval('{[vim.type_idx]=vim.types.dictionary}'))]]))
+ eq(3, eval([[type(luaeval('{[vim.type_idx]=vim.types.array}'))]]))
+
+ eq({}, funcs.luaeval('{[vim.type_idx]=vim.types.array}'))
+
+ -- Presence of type_idx makes Vim ignore some keys
+ eq({42}, funcs.luaeval('{[vim.type_idx]=vim.types.array, [vim.val_idx]=10, [5]=1, foo=2, [1]=42}'))
+ eq({foo=2}, funcs.luaeval('{[vim.type_idx]=vim.types.dictionary, [vim.val_idx]=10, [5]=1, foo=2, [1]=42}'))
+ eq(10, funcs.luaeval('{[vim.type_idx]=vim.types.float, [vim.val_idx]=10, [5]=1, foo=2, [1]=42}'))
+
+ -- The following should not crash
+ eq({}, funcs.luaeval('{[vim.type_idx]=vim.types.dictionary}'))
+ end)
+
+ it('correctly converts self-containing containers', function()
+ meths.set_var('l', {})
+ eval('add(l, l)')
+ eq(true, eval('luaeval("_A == _A[1]", l)'))
+ eq(true, eval('luaeval("_A[1] == _A[1][1]", [l])'))
+ eq(true, eval('luaeval("_A.d == _A.d[1]", {"d": l})'))
+ eq(true, eval('luaeval("_A ~= _A[1]", [l])'))
+
+ meths.set_var('d', {foo=42})
+ eval('extend(d, {"d": d})')
+ eq(true, eval('luaeval("_A == _A.d", d)'))
+ eq(true, eval('luaeval("_A[1] == _A[1].d", [d])'))
+ eq(true, eval('luaeval("_A.d == _A.d.d", {"d": d})'))
+ eq(true, eval('luaeval("_A ~= _A.d", {"d": d})'))
+ end)
+
+ it('errors out correctly when doing incorrect things in lua', function()
+ -- Conversion errors
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: attempt to call field \'xxx_nonexistent_key_xxx\' (a nil value)',
+ exc_exec([[call luaeval("vim.xxx_nonexistent_key_xxx()")]]))
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [string "<VimL compiled string>"]:1: ERROR',
+ exc_exec([[call luaeval("error('ERROR')")]]))
+ eq('Vim(call):E5108: Error while calling lua chunk for luaeval(): [NULL]',
+ exc_exec([[call luaeval("error(nil)")]]))
+ end)
+
+ it('does not leak memory when called with too long line',
+ function()
+ local s = ('x'):rep(65536)
+ eq('Vim(call):E5107: Error while creating lua chunk for luaeval(): [string "<VimL compiled string>"]:1: unexpected symbol near \')\'',
+ exc_exec([[call luaeval("(']] .. s ..[[' + )")]]))
+ eq(s, funcs.luaeval('"' .. s .. '"'))
+ end)
+end)
diff --git a/test/functional/lua/overrides_spec.lua b/test/functional/lua/overrides_spec.lua
new file mode 100644
index 0000000000..c8aee130a7
--- /dev/null
+++ b/test/functional/lua/overrides_spec.lua
@@ -0,0 +1,175 @@
+-- Test for Vim overrides of lua built-ins
+local helpers = require('test.functional.helpers')(after_each)
+local Screen = require('test.functional.ui.screen')
+
+local eq = helpers.eq
+local NIL = helpers.NIL
+local feed = helpers.feed
+local clear = helpers.clear
+local funcs = helpers.funcs
+local meths = helpers.meths
+local command = helpers.command
+local write_file = helpers.write_file
+local redir_exec = helpers.redir_exec
+
+local screen
+
+local fname = 'Xtest-functional-lua-overrides-luafile'
+
+before_each(clear)
+
+after_each(function()
+ os.remove(fname)
+end)
+
+describe('print', function()
+ it('returns nothing', function()
+ eq(NIL, funcs.luaeval('print("abc")'))
+ eq(0, funcs.luaeval('select("#", print("abc"))'))
+ end)
+ it('allows catching printed text with :execute', function()
+ eq('\nabc', funcs.execute('lua print("abc")'))
+ eq('\nabc', funcs.execute('luado print("abc")'))
+ eq('\nabc', funcs.execute('call luaeval("print(\'abc\')")'))
+ write_file(fname, 'print("abc")')
+ eq('\nabc', funcs.execute('luafile ' .. fname))
+
+ eq('\nabc', redir_exec('lua print("abc")'))
+ eq('\nabc', redir_exec('luado print("abc")'))
+ eq('\nabc', redir_exec('call luaeval("print(\'abc\')")'))
+ write_file(fname, 'print("abc")')
+ eq('\nabc', redir_exec('luafile ' .. fname))
+ end)
+ it('handles errors in __tostring', function()
+ write_file(fname, [[
+ local meta_nilerr = { __tostring = function() error(nil) end }
+ local meta_abcerr = { __tostring = function() error("abc") end }
+ local meta_tblout = { __tostring = function() return {"TEST"} end }
+ v_nilerr = setmetatable({}, meta_nilerr)
+ v_abcerr = setmetatable({}, meta_abcerr)
+ v_tblout = setmetatable({}, meta_tblout)
+ ]])
+ eq('', redir_exec('luafile ' .. fname))
+ eq('\nE5114: Error while converting print argument #2: [NULL]',
+ redir_exec('lua print("foo", v_nilerr, "bar")'))
+ eq('\nE5114: Error while converting print argument #2: Xtest-functional-lua-overrides-luafile:2: abc',
+ redir_exec('lua print("foo", v_abcerr, "bar")'))
+ eq('\nE5114: Error while converting print argument #2: <Unknown error: lua_tolstring returned NULL for tostring result>',
+ redir_exec('lua print("foo", v_tblout, "bar")'))
+ end)
+ it('prints strings with NULs and NLs correctly', function()
+ meths.set_option('more', true)
+ eq('\nabc ^@ def\nghi^@^@^@jkl\nTEST\n\n\nT\n',
+ redir_exec([[lua print("abc \0 def\nghi\0\0\0jkl\nTEST\n\n\nT\n")]]))
+ eq('\nabc ^@ def\nghi^@^@^@jkl\nTEST\n\n\nT^@',
+ redir_exec([[lua print("abc \0 def\nghi\0\0\0jkl\nTEST\n\n\nT\0")]]))
+ eq('\nT^@', redir_exec([[lua print("T\0")]]))
+ eq('\nT\n', redir_exec([[lua print("T\n")]]))
+ end)
+end)
+
+describe('debug.debug', function()
+ before_each(function()
+ screen = Screen.new()
+ screen:attach()
+ screen:set_default_attr_ids({
+ [0] = {bold=true, foreground=255},
+ E = {foreground = Screen.colors.Grey100, background = Screen.colors.Red},
+ cr = {bold = true, foreground = Screen.colors.SeaGreen4},
+ })
+ end)
+ it('works', function()
+ command([[lua
+ function Test(a)
+ print(a)
+ debug.debug()
+ print(a * 100)
+ end
+ ]])
+ feed(':lua Test()\n')
+ screen:expect([[
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ nil |
+ lua_debug> ^ |
+ ]])
+ feed('print("TEST")\n')
+ screen:expect([[
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ nil |
+ lua_debug> print("TEST") |
+ TEST |
+ lua_debug> ^ |
+ ]])
+ feed('<C-c>')
+ screen:expect([[
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ nil |
+ lua_debug> print("TEST") |
+ TEST |
+ |
+ {E:E5105: Error while calling lua chunk: [string "<VimL }|
+ {E:compiled string>"]:5: attempt to perform arithmetic o}|
+ {E:n local 'a' (a nil value)} |
+ Interrupt: {cr:Press ENTER or type command to continue}^ |
+ ]])
+ feed('<C-l>:lua Test()\n')
+ screen:expect([[
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ nil |
+ lua_debug> ^ |
+ ]])
+ feed('\n')
+ screen:expect([[
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ {0:~ }|
+ nil |
+ lua_debug> |
+ {E:E5105: Error while calling lua chunk: [string "<VimL }|
+ {E:compiled string>"]:5: attempt to perform arithmetic o}|
+ {E:n local 'a' (a nil value)} |
+ {cr:Press ENTER or type command to continue}^ |
+ ]])
+ end)
+end)
diff --git a/test/helpers.lua b/test/helpers.lua
index d60d5ba242..7a0e4b8c3c 100644
--- a/test/helpers.lua
+++ b/test/helpers.lua
@@ -263,6 +263,14 @@ local function which(exe)
end
end
+local function shallowcopy(orig)
+ local copy = {}
+ for orig_key, orig_value in pairs(orig) do
+ copy[orig_key] = orig_value
+ end
+ return copy
+end
+
local function concat_tables(...)
local ret = {}
for i = 1, select('#', ...) do
@@ -311,6 +319,7 @@ return {
check_cores = check_cores,
hasenv = hasenv,
which = which,
+ shallowcopy = shallowcopy,
concat_tables = concat_tables,
dedent = dedent,
}
diff --git a/third-party/cmake/BuildLua.cmake b/third-party/cmake/BuildLua.cmake
index 1c5e2a186c..ea1371d1d5 100644
--- a/third-party/cmake/BuildLua.cmake
+++ b/third-party/cmake/BuildLua.cmake
@@ -51,19 +51,32 @@ else()
endif()
endif()
+set(LUA_CFLAGS "-O0 -g3 -fPIC")
+set(LUA_LDFLAGS "")
+
+if(CLANG_ASAN_UBSAN)
+ set(LUA_CFLAGS "${LUA_CFLAGS} -fsanitize=address")
+ set(LUA_CFLAGS "${LUA_CFLAGS} -fno-omit-frame-pointer")
+ set(LUA_CFLAGS "${LUA_CFLAGS} -fno-optimize-sibling-calls")
+
+ set(LUA_LDFLAGS "${LUA_LDFLAGS} -fsanitize=address")
+endif()
+
set(LUA_CONFIGURE_COMMAND
sed -e "/^CC/s@gcc@${CMAKE_C_COMPILER} ${CMAKE_C_COMPILER_ARG1}@"
- -e "/^CFLAGS/s@-O2@-g3@"
+ -e "/^CFLAGS/s@-O2@${LUA_CFLAGS}@"
+ -e "/^MYLDFLAGS/s@$@${LUA_LDFLAGS}@"
-e "s@-lreadline@@g"
-e "s@-lhistory@@g"
-e "s@-lncurses@@g"
-i ${DEPS_BUILD_DIR}/src/lua/src/Makefile &&
sed -e "/#define LUA_USE_READLINE/d"
-i ${DEPS_BUILD_DIR}/src/lua/src/luaconf.h)
+set(LUA_INSTALL_TOP_ARG "INSTALL_TOP=${DEPS_INSTALL_DIR}")
set(LUA_BUILD_COMMAND
- ${MAKE_PRG} ${LUA_TARGET})
+ ${MAKE_PRG} ${LUA_INSTALL_TOP_ARG} ${LUA_TARGET})
set(LUA_INSTALL_COMMAND
- ${MAKE_PRG} INSTALL_TOP=${DEPS_INSTALL_DIR} install)
+ ${MAKE_PRG} ${LUA_INSTALL_TOP_ARG} install)
message(STATUS "Lua target is ${LUA_TARGET}")