diff options
-rw-r--r-- | src/nvim/api/vim.c | 141 | ||||
-rw-r--r-- | test/functional/api/vim_spec.lua | 78 |
2 files changed, 188 insertions, 31 deletions
diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index af3d379870..3679d1e569 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -287,21 +287,37 @@ Object nvim_eval(String expr, Error *err) return rv; } -/// Calls a VimL function with the given arguments +/// Execute lua code. Parameters (if any) are available as `...` inside the +/// chunk. The chunk can return a value. /// -/// On VimL error: Returns a generic error; v:errmsg is not updated. +/// Only statements are executed. To evaluate an expression, prefix it +/// with `return`: return my_function(...) /// -/// @param fname Function to call -/// @param args Function arguments packed in an Array +/// @param code lua code to execute +/// @param args Arguments to the code +/// @param[out] err Details of an error encountered while parsing +/// or executing the lua code. +/// +/// @return Return value of lua code if present or NIL. +Object nvim_execute_lua(String code, Array args, Error *err) + FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY +{ + return executor_exec_lua_api(code, args, err); +} + +/// Calls a VimL function. +/// +/// @param fn Function name +/// @param args Function arguments +/// @param self `self` dict, or NULL for non-dict functions /// @param[out] err Error details, if any /// @return Result of the function call -Object nvim_call_function(String fname, Array args, Error *err) - FUNC_API_SINCE(1) +static Object _call_function(String fn, Array args, dict_T *self, Error *err) { Object rv = OBJECT_INIT; if (args.size > MAX_FUNC_ARGS) { api_set_error(err, kErrorTypeValidation, - "Function called with too many arguments."); + "Function called with too many arguments"); return rv; } @@ -318,12 +334,11 @@ Object nvim_call_function(String fname, Array args, Error *err) // Call the function typval_T rettv; int dummy; - int r = call_func((char_u *)fname.data, (int)fname.size, - &rettv, (int)args.size, vim_args, NULL, - curwin->w_cursor.lnum, curwin->w_cursor.lnum, &dummy, - true, NULL, NULL); + int r = call_func((char_u *)fn.data, (int)fn.size, &rettv, (int)args.size, + vim_args, NULL, curwin->w_cursor.lnum, + curwin->w_cursor.lnum, &dummy, true, NULL, self); if (r == FAIL) { - api_set_error(err, kErrorTypeException, "Error calling function."); + api_set_error(err, kErrorTypeException, "Error calling function"); } if (!try_end(err)) { rv = vim_to_object(&rettv); @@ -338,22 +353,100 @@ free_vim_args: return rv; } -/// Execute lua code. Parameters (if any) are available as `...` inside the -/// chunk. The chunk can return a value. +/// Calls a VimL function with the given arguments. /// -/// Only statements are executed. To evaluate an expression, prefix it -/// with `return`: return my_function(...) +/// On VimL error: Returns a generic error; v:errmsg is not updated. /// -/// @param code lua code to execute -/// @param args Arguments to the code -/// @param[out] err Details of an error encountered while parsing -/// or executing the lua code. +/// @param fn Function to call +/// @param args Function arguments packed in an Array +/// @param[out] err Error details, if any +/// @return Result of the function call +Object nvim_call_function(String fn, Array args, Error *err) + FUNC_API_SINCE(1) +{ + return _call_function(fn, args, NULL, err); +} + +/// Calls a VimL |Dictionary-function| with the given arguments. /// -/// @return Return value of lua code if present or NIL. -Object nvim_execute_lua(String code, Array args, Error *err) - FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY +/// @param dict Dictionary, or String evaluating to a VimL |self| dict +/// @param fn Name of the function defined on the VimL dict +/// @param args Function arguments packed in an Array +/// @param[out] err Error details, if any +/// @return Result of the function call +Object nvim_call_dict_function(Object dict, String fn, Array args, Error *err) + FUNC_API_SINCE(4) { - return executor_exec_lua_api(code, args, err); + Object rv = OBJECT_INIT; + + typval_T rettv; + bool mustfree = false; + switch (dict.type) { + case kObjectTypeString: { + try_start(); + if (eval0((char_u *)dict.data.string.data, &rettv, NULL, true) == FAIL) { + api_set_error(err, kErrorTypeException, + "Failed to evaluate dict expression"); + } + if (try_end(err)) { + return rv; + } + // Evaluation of the string arg created a new dict or increased the + // refcount of a dict. Not necessary for a RPC dict. + mustfree = true; + break; + } + case kObjectTypeDictionary: { + if (!object_to_vim(dict, &rettv, err)) { + goto end; + } + break; + } + default: { + api_set_error(err, kErrorTypeValidation, + "dict argument type must be String or Dictionary"); + return rv; + } + } + dict_T *self_dict = rettv.vval.v_dict; + if (rettv.v_type != VAR_DICT || !self_dict) { + api_set_error(err, kErrorTypeValidation, "dict not found"); + goto end; + } + + if (fn.data && fn.size > 0 && dict.type != kObjectTypeDictionary) { + dictitem_T *const di = tv_dict_find(self_dict, fn.data, (ptrdiff_t)fn.size); + if (di == NULL) { + api_set_error(err, kErrorTypeValidation, "Not found: %s", fn.data); + goto end; + } + if (di->di_tv.v_type == VAR_PARTIAL) { + api_set_error(err, kErrorTypeValidation, + "partial function not supported"); + goto end; + } + if (di->di_tv.v_type != VAR_FUNC) { + api_set_error(err, kErrorTypeValidation, "Not a function: %s", fn.data); + goto end; + } + fn = (String) { + .data = (char *)di->di_tv.vval.v_string, + .size = strlen((char *)di->di_tv.vval.v_string), + }; + } + + if (!fn.data || fn.size < 1) { + api_set_error(err, kErrorTypeValidation, "Invalid (empty) function name"); + goto end; + } + + rv = _call_function(fn, args, self_dict, err); +end: + if (mustfree) { + tv_clear(&rettv); + } + + return rv; } /// Calculates the number of display cells occupied by `text`. diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 718294d941..0ff755b320 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -4,17 +4,19 @@ local global_helpers = require('test.helpers') local NIL = helpers.NIL local clear, nvim, eq, neq = helpers.clear, helpers.nvim, helpers.eq, helpers.neq +local command = helpers.command +local funcs = helpers.funcs +local iswin = helpers.iswin +local meth_pcall = helpers.meth_pcall +local meths = helpers.meths local ok, nvim_async, feed = helpers.ok, helpers.nvim_async, helpers.feed local os_name = helpers.os_name -local meths = helpers.meths -local funcs = helpers.funcs local request = helpers.request -local meth_pcall = helpers.meth_pcall -local command = helpers.command -local iswin = helpers.iswin +local source = helpers.source -local intchar2lua = global_helpers.intchar2lua +local expect_err = global_helpers.expect_err local format_string = global_helpers.format_string +local intchar2lua = global_helpers.intchar2lua local mergedicts_copy = global_helpers.mergedicts_copy describe('api', function() @@ -158,13 +160,75 @@ describe('api', function() eq(17, nvim('call_function', 'eval', {17})) eq('foo', nvim('call_function', 'simplify', {'this/./is//redundant/../../../foo'})) end) - it("VimL error: fails (generic error), does NOT update v:errmsg", function() local status, rv = pcall(nvim, "call_function", "bogus function", {"arg1"}) eq(false, status) -- nvim_call_function() failed. ok(nil ~= string.find(rv, "Error calling function")) eq("", nvim("eval", "v:errmsg")) -- v:errmsg was not updated. end) + it('validates args', function() + local too_many_args = { 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x' } + source([[ + function! Foo(...) abort + echo a:000 + endfunction + ]]) + -- E740 + expect_err('Function called with too many arguments', request, + 'nvim_call_function', 'Foo', too_many_args) + end) + end) + + describe('nvim_call_dict_function', function() + it('invokes VimL dict function', function() + source([[ + function! F(name) dict + return self.greeting.', '.a:name.'!' + endfunction + let g:test_dict_fn = { 'greeting':'Hello', 'F':function('F') } + + let g:test_dict_fn2 = { 'greeting':'Hi' } + function g:test_dict_fn2.F2(name) + return self.greeting.', '.a:name.' ...' + endfunction + ]]) + + -- :help Dictionary-function + eq('Hello, World!', nvim('call_dict_function', 'g:test_dict_fn', 'F', {'World'})) + -- Funcref is sent as NIL over RPC. + eq({ greeting = 'Hello', F = NIL }, nvim('get_var', 'test_dict_fn')) + + -- :help numbered-function + eq('Hi, Moon ...', nvim('call_dict_function', 'g:test_dict_fn2', 'F2', {'Moon'})) + -- Funcref is sent as NIL over RPC. + eq({ greeting = 'Hi', F2 = NIL }, nvim('get_var', 'test_dict_fn2')) + + -- Function specified via RPC dict. + source('function! G() dict\n return "@".(self.result)."@"\nendfunction') + eq('@it works@', nvim('call_dict_function', { result = 'it works', G = 'G'}, 'G', {})) + end) + + it('validates args', function() + command('let g:d={"baz":"zub","meep":[]}') + expect_err('Not found: bogus', request, + 'nvim_call_dict_function', 'g:d', 'bogus', {1,2}) + expect_err('Not a function: baz', request, + 'nvim_call_dict_function', 'g:d', 'baz', {1,2}) + expect_err('Not a function: meep', request, + 'nvim_call_dict_function', 'g:d', 'meep', {1,2}) + expect_err('Error calling function', request, + 'nvim_call_dict_function', { f = '' }, 'f', {1,2}) + expect_err('Not a function: f', request, + 'nvim_call_dict_function', "{ 'f': '' }", 'f', {1,2}) + expect_err('dict argument type must be String or Dictionary', request, + 'nvim_call_dict_function', 42, 'f', {1,2}) + expect_err('Failed to evaluate dict expression', request, + 'nvim_call_dict_function', 'foo', 'f', {1,2}) + expect_err('dict not found', request, + 'nvim_call_dict_function', '42', 'f', {1,2}) + expect_err('Invalid %(empty%) function name', request, + 'nvim_call_dict_function', "{ 'f': '' }", '', {1,2}) + end) end) describe('nvim_execute_lua', function() |