diff options
-rw-r--r-- | src/nvim/api/vim.c | 63 | ||||
-rw-r--r-- | src/nvim/ex_cmds2.c | 71 | ||||
-rw-r--r-- | src/nvim/ex_docmd.c | 9 | ||||
-rw-r--r-- | src/nvim/globals.h | 1 | ||||
-rw-r--r-- | test/functional/api/vim_spec.lua | 122 |
5 files changed, 258 insertions, 8 deletions
diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index e4a9bd64ff..6763a3a936 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -25,6 +25,7 @@ #include "nvim/highlight.h" #include "nvim/window.h" #include "nvim/types.h" +#include "nvim/ex_cmds2.h" #include "nvim/ex_docmd.h" #include "nvim/screen.h" #include "nvim/memline.h" @@ -72,10 +73,70 @@ void api_vim_free_all_mem(void) map_free(String, handle_T)(namespace_ids); } +/// Executes Vimscript (multiline block of Ex-commands), like anonymous +/// |:source|. +/// +/// Optionally returns (non-error, non-shell |:!|) output. +/// +/// On execution error: fails with VimL error, does not update v:errmsg. +/// +/// @see |execute()| +/// @see |nvim_command()| +/// +/// @param src Vimscript code +/// @param output Capture and return all (non-error, non-shell |:!|) output +/// @param[out] err Error details (Vim error), if any +String nvim_exec(String src, Boolean output, Error *err) + FUNC_API_SINCE(7) +{ + if (!output) { + try_start(); + do_source_str(src.data, "nvim_exec()"); + try_end(err); + return (String)STRING_INIT; + } + + const int save_msg_silent = msg_silent; + garray_T *const save_capture_ga = capture_ga; + garray_T capture_local; + ga_init(&capture_local, 1, 80); + + try_start(); + msg_silent++; + capture_ga = &capture_local; + do_source_str(src.data, "nvim_exec()"); + capture_ga = save_capture_ga; + msg_silent = save_msg_silent; + try_end(err); + + if (ERROR_SET(err)) { + goto theend; + } + + if (capture_local.ga_len > 1) { + String s = (String){ + .data = capture_local.ga_data, + .size = (size_t)capture_local.ga_len, + }; + // redir usually (except :echon) prepends a newline. + if (s.data[0] == '\n') { + memmove(s.data, s.data + 1, s.size - 1); + s.data[s.size - 1] = '\0'; + s.size = s.size - 1; + } + return s; // Caller will free the memory. + } +theend: + ga_clear(&capture_local); + return (String)STRING_INIT; +} + /// Executes an ex-command. /// /// On execution error: fails with VimL error, does not update v:errmsg. /// +/// @see |nvim_exec()| +/// /// @param command Ex-command string /// @param[out] err Error details (Vim error), if any void nvim_command(String command, Error *err) @@ -378,7 +439,7 @@ theend: return (String)STRING_INIT; } -/// Evaluates a VimL expression (:help expression). +/// Evaluates a VimL |expression|. /// Dictionaries and Lists are recursively expanded. /// /// On execution error: fails with VimL error, does not update v:errmsg. diff --git a/src/nvim/ex_cmds2.c b/src/nvim/ex_cmds2.c index 84291b3637..8479c15b40 100644 --- a/src/nvim/ex_cmds2.c +++ b/src/nvim/ex_cmds2.c @@ -3015,11 +3015,78 @@ static FILE *fopen_noinh_readbin(char *filename) return fdopen(fd_tmp, READBIN); } +typedef struct { + char_u *buf; + size_t offset; +} GetStrLineCookie; -/// Read the file "fname" and execute its lines as EX commands. +/// Get one full line from a sourced string (in-memory, no file). +/// Called by do_cmdline() when it's called from do_source_str(). +/// +/// @return pointer to allocated line, or NULL for end-of-file or +/// some error. +static char_u *get_str_line(int c, void *cookie, int indent, bool do_concat) +{ + GetStrLineCookie *p = cookie; + size_t i = p->offset; + if (strlen((char *)p->buf) <= p->offset) { + return NULL; + } + while (!(p->buf[i] == '\n' || p->buf[i] == '\0')) { + i++; + } + char buf[2046]; + char *dst; + dst = xstpncpy(buf, (char *)p->buf + p->offset, i - p->offset); + if ((uint32_t)(dst - buf) != i - p->offset) { + smsg(_(":source error parsing command %s"), p->buf); + return NULL; + } + buf[i - p->offset] = '\0'; + p->offset = i + 1; + return (char_u *)xstrdup(buf); +} + +/// Executes lines in `src` as Ex commands. +/// +/// @see do_source() +int do_source_str(const char *cmd, const char *traceback_name) +{ + char_u *save_sourcing_name = sourcing_name; + linenr_T save_sourcing_lnum = sourcing_lnum; + char_u sourcing_name_buf[256]; + if (save_sourcing_name == NULL) { + sourcing_name = (char_u *)traceback_name; + } else { + snprintf((char *)sourcing_name_buf, sizeof(sourcing_name_buf), + "%s called at %s:%"PRIdLINENR, traceback_name, save_sourcing_name, + save_sourcing_lnum); + sourcing_name = sourcing_name_buf; + } + sourcing_lnum = 0; + + GetStrLineCookie cookie = { + .buf = (char_u *)cmd, + .offset = 0, + }; + const sctx_T save_current_sctx = current_sctx; + current_sctx.sc_sid = SID_STR; + current_sctx.sc_seq = 0; + current_sctx.sc_lnum = save_sourcing_lnum; + int retval = do_cmdline(NULL, get_str_line, (void *)&cookie, + DOCMD_VERBOSE | DOCMD_NOWAIT | DOCMD_REPEAT); + current_sctx = save_current_sctx; + sourcing_lnum = save_sourcing_lnum; + sourcing_name = save_sourcing_name; + return retval; +} + +/// Reads the file `fname` and executes its lines as Ex commands. /// /// This function may be called recursively! /// +/// @see do_source_str +/// /// @param fname /// @param check_other check for .vimrc and _vimrc /// @param is_vimrc DOSO_ value @@ -3360,6 +3427,8 @@ char_u *get_scriptname(LastSet last_set, bool *should_free) _("API client (channel id %" PRIu64 ")"), last_set.channel_id); return IObuff; + case SID_STR: + return (char_u *)_(":source (no file)"); default: *should_free = true; return home_replace_save(NULL, diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 743286c64a..dc2726709f 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -421,13 +421,12 @@ int do_cmdline(char_u *cmdline, LineGetter fgetline, // If force_abort is set, we cancel everything. did_emsg = false; - /* - * KeyTyped is only set when calling vgetc(). Reset it here when not - * calling vgetc() (sourced command lines). - */ + // KeyTyped is only set when calling vgetc(). Reset it here when not + // calling vgetc() (sourced command lines). if (!(flags & DOCMD_KEYTYPED) - && !getline_equal(fgetline, cookie, getexline)) + && !getline_equal(fgetline, cookie, getexline)) { KeyTyped = false; + } /* * Continue executing command lines: diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 15ad6d8767..172c190df2 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -331,6 +331,7 @@ EXTERN int garbage_collect_at_exit INIT(= false); #define SID_NONE -6 // don't set scriptID #define SID_LUA -7 // for Lua scripts/chunks #define SID_API_CLIENT -8 // for API clients +#define SID_STR -9 // for sourcing a string // Script CTX being sourced or was sourced to define the current function. EXTERN sctx_T current_sctx INIT(= { 0 COMMA 0 COMMA 0 }); diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 8b77dbcaa6..d7baf68a5b 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -16,6 +16,8 @@ local parse_context = helpers.parse_context local request = helpers.request local source = helpers.source local next_msg = helpers.next_msg +local tmpname = helpers.tmpname +local write_file = helpers.write_file local pcall_err = helpers.pcall_err local format_string = helpers.format_string @@ -74,9 +76,127 @@ describe('API', function() eq({mode='i', blocking=false}, nvim("get_mode")) end) + describe('nvim_exec', function() + it('one-line input', function() + nvim('exec', "let x1 = 'a'", false) + eq('a', nvim('get_var', 'x1')) + end) + + it(':verbose set {option}?', function() + nvim('exec', 'set nowrap', false) + eq('nowrap\n\tLast set from :source (no file)', + nvim('exec', 'verbose set wrap?', true)) + end) + + it('multiline input', function() + -- Heredoc + empty lines. + nvim('exec', "let x2 = 'a'\n", false) + eq('a', nvim('get_var', 'x2')) + nvim('exec','lua <<EOF\n\n\n\ny=3\n\n\nEOF', false) + eq(3, nvim('eval', "luaeval('y')")) + + eq('', nvim('exec', 'lua <<EOF\ny=3\nEOF', false)) + eq(3, nvim('eval', "luaeval('y')")) + + -- Multiple statements + nvim('exec', 'let x1=1\nlet x2=2\nlet x3=3\n', false) + eq(1, nvim('eval', 'x1')) + eq(2, nvim('eval', 'x2')) + eq(3, nvim('eval', 'x3')) + + -- Functions + nvim('exec', 'function Foo()\ncall setline(1,["xxx"])\nendfunction', false) + eq(nvim('get_current_line'), '') + nvim('exec', 'call Foo()', false) + eq(nvim('get_current_line'), 'xxx') + + -- Autocmds + nvim('exec','autocmd BufAdd * :let x1 = "Hello"', false) + nvim('command', 'new foo') + eq('Hello', request('nvim_eval', 'g:x1')) + end) + + it('non-ASCII input', function() + nvim('exec', [=[ + new + exe "normal! i ax \n Ax " + :%s/ax/--a1234--/g | :%s/Ax/--A1234--/g + ]=], false) + nvim('command', '1') + eq(' --a1234-- ', nvim('get_current_line')) + nvim('command', '2') + eq(' --A1234-- ', nvim('get_current_line')) + + nvim('exec', [[ + new + call setline(1,['xxx']) + call feedkeys('r') + call feedkeys('ñ', 'xt') + ]], false) + eq('ñxx', nvim('get_current_line')) + end) + + it('execution error', function() + eq('Vim:E492: Not an editor command: bogus_command', + pcall_err(request, 'nvim_exec', 'bogus_command', false)) + eq('', nvim('eval', 'v:errmsg')) -- v:errmsg was not updated. + eq('', eval('v:exception')) + + eq('Vim(buffer):E86: Buffer 23487 does not exist', + pcall_err(request, 'nvim_exec', 'buffer 23487', false)) + eq('', eval('v:errmsg')) -- v:errmsg was not updated. + eq('', eval('v:exception')) + end) + + it('recursion', function() + local fname = tmpname() + write_file(fname, 'let x1 = "set from :source file"\n') + -- nvim_exec + -- :source + -- nvim_exec + request('nvim_exec', [[ + let x2 = substitute('foo','o','X','g') + let x4 = 'should be overwritten' + call nvim_exec("source ]]..fname..[[\nlet x3 = substitute('foo','foo','set by recursive nvim_exec','g')\nlet x5='overwritten'\nlet x4=x5\n", v:false) + ]], false) + eq('set from :source file', request('nvim_get_var', 'x1')) + eq('fXX', request('nvim_get_var', 'x2')) + eq('set by recursive nvim_exec', request('nvim_get_var', 'x3')) + eq('overwritten', request('nvim_get_var', 'x4')) + eq('overwritten', request('nvim_get_var', 'x5')) + os.remove(fname) + end) + + it('traceback', function() + local fname = tmpname() + write_file(fname, 'echo "hello"\n') + local sourcing_fname = tmpname() + write_file(sourcing_fname, 'call nvim_exec("source '..fname..'", v:false)\n') + meths.exec('set verbose=2', false) + local traceback_output = 'line 0: sourcing "'..sourcing_fname..'"\n'.. + 'line 0: sourcing "'..fname..'"\n'.. + 'hello\n'.. + 'finished sourcing '..fname..'\n'.. + 'continuing in nvim_exec() called at '..sourcing_fname..':1\n'.. + 'finished sourcing '..sourcing_fname..'\n'.. + 'continuing in nvim_exec() called at nvim_exec():0' + eq(traceback_output, + meths.exec('call nvim_exec("source '..sourcing_fname..'", v:false)', true)) + os.remove(fname) + os.remove(sourcing_fname) + end) + + it('returns output', function() + eq('this is spinal tap', + nvim('exec', 'lua <<EOF\n\n\nprint("this is spinal tap")\n\n\nEOF', true)) + eq('', nvim('exec', 'echo', true)) + eq('foo 42', nvim('exec', 'echo "foo" 42', true)) + end) + end) + describe('nvim_command', function() it('works', function() - local fname = helpers.tmpname() + local fname = tmpname() nvim('command', 'new') nvim('command', 'edit '..fname) nvim('command', 'normal itesting\napi') |