diff options
-rw-r--r-- | runtime/doc/builtin.txt | 15 | ||||
-rw-r--r-- | runtime/doc/eval.txt | 3 | ||||
-rw-r--r-- | runtime/doc/usr_41.txt | 3 | ||||
-rw-r--r-- | runtime/doc/vvars.txt | 15 | ||||
-rw-r--r-- | runtime/lua/vim/_meta/vimfn.lua | 14 | ||||
-rw-r--r-- | runtime/lua/vim/_meta/vvars.lua | 12 | ||||
-rw-r--r-- | src/nvim/eval.c | 1 | ||||
-rw-r--r-- | src/nvim/eval.h | 1 | ||||
-rw-r--r-- | src/nvim/eval.lua | 19 | ||||
-rw-r--r-- | src/nvim/eval/typval.c | 24 | ||||
-rw-r--r-- | src/nvim/ex_eval.c | 7 | ||||
-rw-r--r-- | src/nvim/ex_eval_defs.h | 2 | ||||
-rw-r--r-- | src/nvim/runtime.c | 66 | ||||
-rw-r--r-- | src/nvim/vvars.lua | 14 | ||||
-rw-r--r-- | test/old/testdir/test_stacktrace.vim | 107 |
15 files changed, 294 insertions, 9 deletions
diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index f321c880a4..70c7a7b802 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -4182,6 +4182,21 @@ getscriptinfo([{opts}]) *getscriptinfo()* Return: ~ (`vim.fn.getscriptinfo.ret[]`) +getstacktrace() *getstacktrace()* + Returns the current stack trace of Vim scripts. + Stack trace is a |List|, of which each item is a |Dictionary| + with the following items: + funcref The funcref if the stack is at the function, + otherwise this item is not exist. + event The string of the event description if the + stack is at autocmd event, otherwise this item + is not exist. + lnum The line number of the script on the stack. + filepath The file path of the script on the stack. + + Return: ~ + (`table[]`) + gettabinfo([{tabnr}]) *gettabinfo()* If {tabnr} is not specified, then information about all the tab pages is returned as a |List|. Each List item is a diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index e0c45503cc..60238bc90d 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -2848,7 +2848,8 @@ in the variable |v:exception|: > : echo "Number thrown. Value is" v:exception You may also be interested where an exception was thrown. This is stored in -|v:throwpoint|. Note that "v:exception" and "v:throwpoint" are valid for the +|v:throwpoint|. And you can obtain the stack trace from |v:stacktrace|. +Note that "v:exception", "v:stacktrace" and "v:throwpoint" are valid for the exception most recently caught as long it is not finished. Example: > diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt index 3202a70b76..f958491ccf 100644 --- a/runtime/doc/usr_41.txt +++ b/runtime/doc/usr_41.txt @@ -1103,7 +1103,8 @@ Various: *various-functions* did_filetype() check if a FileType autocommand was used eventhandler() check if invoked by an event handler getpid() get process ID of Vim - getscriptinfo() get list of sourced vim scripts + getscriptinfo() get list of sourced Vim scripts + getstacktrace() get current stack trace of Vim scripts libcall() call a function in an external library libcallnr() idem, returning a number diff --git a/runtime/doc/vvars.txt b/runtime/doc/vvars.txt index 32f3b96269..0ebb54e38a 100644 --- a/runtime/doc/vvars.txt +++ b/runtime/doc/vvars.txt @@ -6,7 +6,8 @@ Predefined variables *vvars* -Some variables can be set by the user, but the type cannot be changed. +Most variables are read-only, when a variable can be set by the user, it will +be mentioned at the variable description below. The type cannot be changed. Type |gO| to see the table of contents. @@ -195,7 +196,8 @@ v:event *v:exception* *exception-variable* v:exception The value of the exception most recently caught and not - finished. See also |v:throwpoint| and |throw-variables|. + finished. See also |v:stacktrace|, |v:throwpoint|, and + |throw-variables|. Example: >vim try throw "oops" @@ -586,6 +588,13 @@ v:shell_error endif < + *v:stacktrace* *stacktrace-variable* +v:stacktrace + The stack trace of the exception most recently caught and + not finished. Refer to |getstacktrace()| for the structure of + stack trace. See also |v:exception|, |v:throwpoint|, and + |throw-variables|. + *v:statusmsg* *statusmsg-variable* v:statusmsg Last given status message. @@ -679,7 +688,7 @@ v:this_session v:throwpoint The point where the exception most recently caught and not finished was thrown. Not set when commands are typed. See - also |v:exception| and |throw-variables|. + also |v:exception|, |v:stacktrace|, and |throw-variables|. Example: >vim try throw "oops" diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua index 6662fca84f..3de8b9951c 100644 --- a/runtime/lua/vim/_meta/vimfn.lua +++ b/runtime/lua/vim/_meta/vimfn.lua @@ -3770,6 +3770,20 @@ function vim.fn.getregtype(regname) end --- @return vim.fn.getscriptinfo.ret[] function vim.fn.getscriptinfo(opts) end +--- Returns the current stack trace of Vim scripts. +--- Stack trace is a |List|, of which each item is a |Dictionary| +--- with the following items: +--- funcref The funcref if the stack is at the function, +--- otherwise this item is not exist. +--- event The string of the event description if the +--- stack is at autocmd event, otherwise this item +--- is not exist. +--- lnum The line number of the script on the stack. +--- filepath The file path of the script on the stack. +--- +--- @return table[] +function vim.fn.getstacktrace() end + --- If {tabnr} is not specified, then information about all the --- tab pages is returned as a |List|. Each List item is a --- |Dictionary|. Otherwise, {tabnr} specifies the tab page diff --git a/runtime/lua/vim/_meta/vvars.lua b/runtime/lua/vim/_meta/vvars.lua index 445da4e02f..c1b8695bbf 100644 --- a/runtime/lua/vim/_meta/vvars.lua +++ b/runtime/lua/vim/_meta/vvars.lua @@ -203,7 +203,8 @@ vim.v.errors = ... vim.v.event = ... --- The value of the exception most recently caught and not ---- finished. See also `v:throwpoint` and `throw-variables`. +--- finished. See also `v:stacktrace`, `v:throwpoint`, and +--- `throw-variables`. --- Example: --- --- ```vim @@ -616,6 +617,13 @@ vim.v.servername = ... --- @type integer vim.v.shell_error = ... +--- The stack trace of the exception most recently caught and +--- not finished. Refer to `getstacktrace()` for the structure of +--- stack trace. See also `v:exception`, `v:throwpoint`, and +--- `throw-variables`. +--- @type table[] +vim.v.stacktrace = ... + --- Last given status message. --- Modifiable (can be set). --- @type string @@ -718,7 +726,7 @@ vim.v.this_session = ... --- The point where the exception most recently caught and not --- finished was thrown. Not set when commands are typed. See ---- also `v:exception` and `throw-variables`. +--- also `v:exception`, `v:stacktrace`, and `throw-variables`. --- Example: --- --- ```vim diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 8bdd8dad4c..a90f275713 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -270,6 +270,7 @@ static struct vimvar { VV(VV_COLLATE, "collate", VAR_STRING, VV_RO), VV(VV_EXITING, "exiting", VAR_NUMBER, VV_RO), VV(VV_MAXCOL, "maxcol", VAR_NUMBER, VV_RO), + VV(VV_STACKTRACE, "stacktrace", VAR_LIST, VV_RO), // Neovim VV(VV_STDERR, "stderr", VAR_NUMBER, VV_RO), VV(VV_MSGPACK_TYPES, "msgpack_types", VAR_DICT, VV_RO), diff --git a/src/nvim/eval.h b/src/nvim/eval.h index bb9b00abc7..8b4aa8101a 100644 --- a/src/nvim/eval.h +++ b/src/nvim/eval.h @@ -167,6 +167,7 @@ typedef enum { VV_COLLATE, VV_EXITING, VV_MAXCOL, + VV_STACKTRACE, // Nvim VV_STDERR, VV_MSGPACK_TYPES, diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index c650dee306..5901ed5766 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -4670,6 +4670,25 @@ M.funcs = { returns = 'vim.fn.getscriptinfo.ret[]', signature = 'getscriptinfo([{opts}])', }, + getstacktrace = { + args = 0, + desc = [=[ + Returns the current stack trace of Vim scripts. + Stack trace is a |List|, of which each item is a |Dictionary| + with the following items: + funcref The funcref if the stack is at the function, + otherwise this item is not exist. + event The string of the event description if the + stack is at autocmd event, otherwise this item + is not exist. + lnum The line number of the script on the stack. + filepath The file path of the script on the stack. + ]=], + name = 'getstacktrace', + params = {}, + returns = 'table[]', + signature = 'getstacktrace()', + }, gettabinfo = { args = { 0, 1 }, base = 1, diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index cbb6b5644f..ed1031577c 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -2633,6 +2633,30 @@ int tv_dict_add_allocated_str(dict_T *const d, const char *const key, const size return OK; } +/// Add a function entry to dictionary. +/// +/// @param[out] d Dictionary to add entry to. +/// @param[in] key Key to add. +/// @param[in] key_len Key length. +/// @param[in] fp Function to add. +/// +/// @return OK in case of success, FAIL when key already exists. +int tv_dict_add_func(dict_T *const d, const char *const key, const size_t key_len, + ufunc_T *const fp) + FUNC_ATTR_NONNULL_ARG(1, 2, 4) +{ + dictitem_T *const item = tv_dict_item_alloc_len(key, key_len); + + item->di_tv.v_type = VAR_FUNC; + item->di_tv.vval.v_string = xstrdup(fp->uf_name); + if (tv_dict_add(d, item) == FAIL) { + tv_dict_item_free(item); + return FAIL; + } + func_ref(item->di_tv.vval.v_string); + return OK; +} + //{{{2 Operations on the whole dict /// Clear all the keys of a Dictionary. "d" remains a valid empty Dictionary. diff --git a/src/nvim/ex_eval.c b/src/nvim/ex_eval.c index f9936dd88e..18c691d076 100644 --- a/src/nvim/ex_eval.c +++ b/src/nvim/ex_eval.c @@ -479,6 +479,9 @@ static int throw_exception(void *value, except_type_T type, char *cmdname) excp->throw_lnum = SOURCING_LNUM; } + excp->stacktrace = stacktrace_create(); + tv_list_ref(excp->stacktrace); + if (p_verbose >= 13 || debug_break_level > 0) { int save_msg_silent = msg_silent; @@ -563,6 +566,7 @@ static void discard_exception(except_T *excp, bool was_finished) free_msglist(excp->messages); } xfree(excp->throw_name); + tv_list_unref(excp->stacktrace); xfree(excp); } @@ -584,6 +588,7 @@ static void catch_exception(except_T *excp) excp->caught = caught_stack; caught_stack = excp; set_vim_var_string(VV_EXCEPTION, excp->value, -1); + set_vim_var_list(VV_STACKTRACE, excp->stacktrace); if (*excp->throw_name != NUL) { if (excp->throw_lnum != 0) { vim_snprintf(IObuff, IOSIZE, _("%s, line %" PRId64), @@ -633,6 +638,7 @@ static void finish_exception(except_T *excp) caught_stack = caught_stack->caught; if (caught_stack != NULL) { set_vim_var_string(VV_EXCEPTION, caught_stack->value, -1); + set_vim_var_list(VV_STACKTRACE, caught_stack->stacktrace); if (*caught_stack->throw_name != NUL) { if (caught_stack->throw_lnum != 0) { vim_snprintf(IObuff, IOSIZE, @@ -651,6 +657,7 @@ static void finish_exception(except_T *excp) } else { set_vim_var_string(VV_EXCEPTION, NULL, -1); set_vim_var_string(VV_THROWPOINT, NULL, -1); + set_vim_var_list(VV_STACKTRACE, NULL); } // Discard the exception, but use the finish message for 'verbose'. diff --git a/src/nvim/ex_eval_defs.h b/src/nvim/ex_eval_defs.h index 3f5e510a20..e0d06f3e93 100644 --- a/src/nvim/ex_eval_defs.h +++ b/src/nvim/ex_eval_defs.h @@ -2,6 +2,7 @@ #include <stdbool.h> +#include "nvim/eval/typval_defs.h" #include "nvim/pos_defs.h" /// A list used for saving values of "emsg_silent". Used by ex_try() to save the @@ -107,6 +108,7 @@ struct vim_exception { msglist_T *messages; ///< message(s) causing error exception char *throw_name; ///< name of the throw point linenr_T throw_lnum; ///< line number of the throw point + list_T *stacktrace; ///< stacktrace except_T *caught; ///< next exception on the caught stack }; diff --git a/src/nvim/runtime.c b/src/nvim/runtime.c index d849a18879..cdedf86977 100644 --- a/src/nvim/runtime.c +++ b/src/nvim/runtime.c @@ -228,6 +228,72 @@ char *estack_sfile(estack_arg_T which) return (char *)ga.ga_data; } +static void stacktrace_push_item(list_T *const l, ufunc_T *const fp, const char *const event, + const linenr_T lnum, char *const filepath, + const bool filepath_alloced) +{ + dict_T *const d = tv_dict_alloc_lock(VAR_FIXED); + typval_T tv = { + .v_type = VAR_DICT, + .v_lock = VAR_LOCKED, + .vval.v_dict = d, + }; + + if (fp != NULL) { + tv_dict_add_func(d, S_LEN("funcref"), fp); + } + if (event != NULL) { + tv_dict_add_str(d, S_LEN("event"), event); + } + tv_dict_add_nr(d, S_LEN("lnum"), lnum); + if (filepath_alloced) { + tv_dict_add_allocated_str(d, S_LEN("filepath"), filepath); + } else { + tv_dict_add_str(d, S_LEN("filepath"), filepath); + } + + tv_list_append_tv(l, &tv); +} + +/// Create the stacktrace from exestack. +list_T *stacktrace_create(void) +{ + list_T *const l = tv_list_alloc(exestack.ga_len); + + for (int i = 0; i < exestack.ga_len; i++) { + estack_T *const entry = &((estack_T *)exestack.ga_data)[i]; + linenr_T lnum = entry->es_lnum; + + if (entry->es_type == ETYPE_SCRIPT) { + stacktrace_push_item(l, NULL, NULL, lnum, entry->es_name, false); + } else if (entry->es_type == ETYPE_UFUNC) { + ufunc_T *const fp = entry->es_info.ufunc; + const sctx_T sctx = fp->uf_script_ctx; + bool filepath_alloced = false; + char *filepath = sctx.sc_sid > 0 + ? get_scriptname((LastSet){ .script_ctx = sctx }, + &filepath_alloced) : ""; + lnum += sctx.sc_lnum; + stacktrace_push_item(l, fp, NULL, lnum, filepath, filepath_alloced); + } else if (entry->es_type == ETYPE_AUCMD) { + const sctx_T sctx = entry->es_info.aucmd->script_ctx; + bool filepath_alloced = false; + char *filepath = sctx.sc_sid > 0 + ? get_scriptname((LastSet){ .script_ctx = sctx }, + &filepath_alloced) : ""; + lnum += sctx.sc_lnum; + stacktrace_push_item(l, NULL, entry->es_name, lnum, filepath, filepath_alloced); + } + } + return l; +} + +/// getstacktrace() function +void f_getstacktrace(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) +{ + tv_list_set_ret(rettv, stacktrace_create()); +} + static bool runtime_search_path_valid = false; static int *runtime_search_path_ref = NULL; static RuntimeSearchPath runtime_search_path; diff --git a/src/nvim/vvars.lua b/src/nvim/vvars.lua index e705c02e83..056e281c0b 100644 --- a/src/nvim/vvars.lua +++ b/src/nvim/vvars.lua @@ -220,7 +220,8 @@ M.vars = { type = 'string', desc = [=[ The value of the exception most recently caught and not - finished. See also |v:throwpoint| and |throw-variables|. + finished. See also |v:stacktrace|, |v:throwpoint|, and + |throw-variables|. Example: >vim try throw "oops" @@ -701,6 +702,15 @@ M.vars = { < ]=], }, + stacktrace = { + type = 'table[]', + desc = [=[ + The stack trace of the exception most recently caught and + not finished. Refer to |getstacktrace()| for the structure of + stack trace. See also |v:exception|, |v:throwpoint|, and + |throw-variables|. + ]=], + }, statusmsg = { type = 'string', desc = [=[ @@ -823,7 +833,7 @@ M.vars = { desc = [=[ The point where the exception most recently caught and not finished was thrown. Not set when commands are typed. See - also |v:exception| and |throw-variables|. + also |v:exception|, |v:stacktrace|, and |throw-variables|. Example: >vim try throw "oops" diff --git a/test/old/testdir/test_stacktrace.vim b/test/old/testdir/test_stacktrace.vim new file mode 100644 index 0000000000..2ff5801ce6 --- /dev/null +++ b/test/old/testdir/test_stacktrace.vim @@ -0,0 +1,107 @@ +" Test for getstacktrace() and v:stacktrace + +let s:thisfile = expand('%:p') +let s:testdir = s:thisfile->fnamemodify(':h') + +func Filepath(name) + return s:testdir .. '/' .. a:name +endfunc + +func AssertStacktrace(expect, actual) + call assert_equal(#{lnum: 581, filepath: Filepath('runtest.vim')}, a:actual[0]) + call assert_equal(a:expect, a:actual[-len(a:expect):]) +endfunc + +func Test_getstacktrace() + let g:stacktrace = [] + let lines1 =<< trim [SCRIPT] + " Xscript1 + source Xscript2 + func Xfunc1() + " Xfunc1 + call Xfunc2() + endfunc + [SCRIPT] + let lines2 =<< trim [SCRIPT] + " Xscript2 + func Xfunc2() + " Xfunc2 + let g:stacktrace = getstacktrace() + endfunc + [SCRIPT] + call writefile(lines1, 'Xscript1', 'D') + call writefile(lines2, 'Xscript2', 'D') + source Xscript1 + call Xfunc1() + call AssertStacktrace([ + \ #{funcref: funcref('Test_getstacktrace'), lnum: 35, filepath: s:thisfile}, + \ #{funcref: funcref('Xfunc1'), lnum: 5, filepath: Filepath('Xscript1')}, + \ #{funcref: funcref('Xfunc2'), lnum: 4, filepath: Filepath('Xscript2')}, + \ ], g:stacktrace) + unlet g:stacktrace +endfunc + +func Test_getstacktrace_event() + let g:stacktrace = [] + let lines1 =<< trim [SCRIPT] + " Xscript1 + func Xfunc() + " Xfunc + let g:stacktrace = getstacktrace() + endfunc + augroup test_stacktrace + autocmd SourcePre * call Xfunc() + augroup END + [SCRIPT] + let lines2 =<< trim [SCRIPT] + " Xscript2 + [SCRIPT] + call writefile(lines1, 'Xscript1', 'D') + call writefile(lines2, 'Xscript2', 'D') + source Xscript1 + source Xscript2 + call AssertStacktrace([ + \ #{funcref: funcref('Test_getstacktrace_event'), lnum: 62, filepath: s:thisfile}, + \ #{event: 'SourcePre Autocommands for "*"', lnum: 7, filepath: Filepath('Xscript1')}, + \ #{funcref: funcref('Xfunc'), lnum: 4, filepath: Filepath('Xscript1')}, + \ ], g:stacktrace) + augroup test_stacktrace + autocmd! + augroup END + unlet g:stacktrace +endfunc + +func Test_vstacktrace() + let lines1 =<< trim [SCRIPT] + " Xscript1 + source Xscript2 + func Xfunc1() + " Xfunc1 + call Xfunc2() + endfunc + [SCRIPT] + let lines2 =<< trim [SCRIPT] + " Xscript2 + func Xfunc2() + " Xfunc2 + throw 'Exception from Xfunc2' + endfunc + [SCRIPT] + call writefile(lines1, 'Xscript1', 'D') + call writefile(lines2, 'Xscript2', 'D') + source Xscript1 + call assert_equal([], v:stacktrace) + try + call Xfunc1() + catch + let stacktrace = v:stacktrace + endtry + call assert_equal([], v:stacktrace) + call AssertStacktrace([ + \ #{funcref: funcref('Test_vstacktrace'), lnum: 95, filepath: s:thisfile}, + \ #{funcref: funcref('Xfunc1'), lnum: 5, filepath: Filepath('Xscript1')}, + \ #{funcref: funcref('Xfunc2'), lnum: 4, filepath: Filepath('Xscript2')}, + \ ], stacktrace) +endfunc + +" vim: shiftwidth=2 sts=2 expandtab |