aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorzeertzjq <zeertzjq@outlook.com>2025-01-03 20:12:15 +0800
committerzeertzjq <zeertzjq@outlook.com>2025-01-07 09:15:10 +0800
commitd5308637bf1aac2b97fccf73a0ffdef304eaa1d6 (patch)
treeacb03c837290a225f1fcb7f73b4ae1306d1e7589
parent06ff5480ce274daf3b7ad9950a587099200dc8ff (diff)
downloadrneovim-d5308637bf1aac2b97fccf73a0ffdef304eaa1d6.tar.gz
rneovim-d5308637bf1aac2b97fccf73a0ffdef304eaa1d6.tar.bz2
rneovim-d5308637bf1aac2b97fccf73a0ffdef304eaa1d6.zip
vim-patch:9.1.0984: exception handling can be improved
Problem: exception handling can be improved Solution: add v:stacktrace and getstacktrace() closes: vim/vim#16360 https://github.com/vim/vim/commit/663d18d6102f40d14e36096ec590445e61026ed6 Co-authored-by: ichizok <gclient.gaap@gmail.com> Co-authored-by: Naruhiko Nishino <naru123456789@gmail.com>
-rw-r--r--runtime/doc/builtin.txt15
-rw-r--r--runtime/doc/eval.txt3
-rw-r--r--runtime/doc/usr_41.txt3
-rw-r--r--runtime/doc/vvars.txt15
-rw-r--r--runtime/lua/vim/_meta/vimfn.lua14
-rw-r--r--runtime/lua/vim/_meta/vvars.lua12
-rw-r--r--src/nvim/eval.c1
-rw-r--r--src/nvim/eval.h1
-rw-r--r--src/nvim/eval.lua19
-rw-r--r--src/nvim/eval/typval.c24
-rw-r--r--src/nvim/ex_eval.c7
-rw-r--r--src/nvim/ex_eval_defs.h2
-rw-r--r--src/nvim/runtime.c66
-rw-r--r--src/nvim/vvars.lua14
-rw-r--r--test/old/testdir/test_stacktrace.vim107
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