mirror of
https://github.com/neovim/neovim.git
synced 2025-09-18 09:18:19 +00:00
fix(exceptions): restore did_throw
(#20000)
`!did_throw` doesn't exactly imply `!current_exception`, as `did_throw = false` is sometimes used to defer exception handling for later (without forgetting the exception). E.g: uncaught exception handling in `do_cmdline()` may be deferred to a different call (e.g: when `try_level > 0`). In #7881, `current_exception = NULL` in `do_cmdline()` is used as an analogue of `did_throw = false`, but also causes the pending exception to be lost, which also leaks as `discard_exception()` wasn't used. It may be possible to fix this by saving/restoring `current_exception`, but handling all of `did_throw`'s edge cases seems messier. Maybe not worth diverging over. This fix also uncovers a `man_spec.lua` bug on Windows: exceptions are thrown due to Windows missing `man`, but they're lost; skip these tests if `man` isn't executable.
This commit is contained in:
@@ -58,6 +58,7 @@ void try_enter(TryState *const tstate)
|
|||||||
.private_msg_list = NULL,
|
.private_msg_list = NULL,
|
||||||
.trylevel = trylevel,
|
.trylevel = trylevel,
|
||||||
.got_int = got_int,
|
.got_int = got_int,
|
||||||
|
.did_throw = did_throw,
|
||||||
.need_rethrow = need_rethrow,
|
.need_rethrow = need_rethrow,
|
||||||
.did_emsg = did_emsg,
|
.did_emsg = did_emsg,
|
||||||
};
|
};
|
||||||
@@ -65,6 +66,7 @@ void try_enter(TryState *const tstate)
|
|||||||
current_exception = NULL;
|
current_exception = NULL;
|
||||||
trylevel = 1;
|
trylevel = 1;
|
||||||
got_int = false;
|
got_int = false;
|
||||||
|
did_throw = false;
|
||||||
need_rethrow = false;
|
need_rethrow = false;
|
||||||
did_emsg = false;
|
did_emsg = false;
|
||||||
}
|
}
|
||||||
@@ -85,6 +87,7 @@ bool try_leave(const TryState *const tstate, Error *const err)
|
|||||||
assert(trylevel == 0);
|
assert(trylevel == 0);
|
||||||
assert(!need_rethrow);
|
assert(!need_rethrow);
|
||||||
assert(!got_int);
|
assert(!got_int);
|
||||||
|
assert(!did_throw);
|
||||||
assert(!did_emsg);
|
assert(!did_emsg);
|
||||||
assert(msg_list == &tstate->private_msg_list);
|
assert(msg_list == &tstate->private_msg_list);
|
||||||
assert(*msg_list == NULL);
|
assert(*msg_list == NULL);
|
||||||
@@ -93,6 +96,7 @@ bool try_leave(const TryState *const tstate, Error *const err)
|
|||||||
current_exception = tstate->current_exception;
|
current_exception = tstate->current_exception;
|
||||||
trylevel = tstate->trylevel;
|
trylevel = tstate->trylevel;
|
||||||
got_int = tstate->got_int;
|
got_int = tstate->got_int;
|
||||||
|
did_throw = tstate->did_throw;
|
||||||
need_rethrow = tstate->need_rethrow;
|
need_rethrow = tstate->need_rethrow;
|
||||||
did_emsg = tstate->did_emsg;
|
did_emsg = tstate->did_emsg;
|
||||||
return ret;
|
return ret;
|
||||||
@@ -127,7 +131,7 @@ bool try_end(Error *err)
|
|||||||
force_abort = false;
|
force_abort = false;
|
||||||
|
|
||||||
if (got_int) {
|
if (got_int) {
|
||||||
if (current_exception) {
|
if (did_throw) {
|
||||||
// If we got an interrupt, discard the current exception
|
// If we got an interrupt, discard the current exception
|
||||||
discard_current_exception();
|
discard_current_exception();
|
||||||
}
|
}
|
||||||
@@ -146,7 +150,7 @@ bool try_end(Error *err)
|
|||||||
if (should_free) {
|
if (should_free) {
|
||||||
xfree(msg);
|
xfree(msg);
|
||||||
}
|
}
|
||||||
} else if (current_exception) {
|
} else if (did_throw) {
|
||||||
api_set_error(err, kErrorTypeException, "%s", current_exception->value);
|
api_set_error(err, kErrorTypeException, "%s", current_exception->value);
|
||||||
discard_current_exception();
|
discard_current_exception();
|
||||||
}
|
}
|
||||||
|
@@ -134,6 +134,7 @@ typedef struct {
|
|||||||
const msglist_T *const *msg_list;
|
const msglist_T *const *msg_list;
|
||||||
int trylevel;
|
int trylevel;
|
||||||
int got_int;
|
int got_int;
|
||||||
|
bool did_throw;
|
||||||
int need_rethrow;
|
int need_rethrow;
|
||||||
int did_emsg;
|
int did_emsg;
|
||||||
} TryState;
|
} TryState;
|
||||||
|
@@ -137,7 +137,7 @@ Object nvim_eval(String expr, Error *err)
|
|||||||
if (!recursive) {
|
if (!recursive) {
|
||||||
force_abort = false;
|
force_abort = false;
|
||||||
suppress_errthrow = false;
|
suppress_errthrow = false;
|
||||||
current_exception = NULL;
|
did_throw = false;
|
||||||
// `did_emsg` is set by emsg(), which cancels execution.
|
// `did_emsg` is set by emsg(), which cancels execution.
|
||||||
did_emsg = false;
|
did_emsg = false;
|
||||||
}
|
}
|
||||||
@@ -196,7 +196,7 @@ static Object _call_function(String fn, Array args, dict_T *self, Error *err)
|
|||||||
if (!recursive) {
|
if (!recursive) {
|
||||||
force_abort = false;
|
force_abort = false;
|
||||||
suppress_errthrow = false;
|
suppress_errthrow = false;
|
||||||
current_exception = NULL;
|
did_throw = false;
|
||||||
// `did_emsg` is set by emsg(), which cancels execution.
|
// `did_emsg` is set by emsg(), which cancels execution.
|
||||||
did_emsg = false;
|
did_emsg = false;
|
||||||
}
|
}
|
||||||
|
@@ -5985,7 +5985,7 @@ void timer_due_cb(TimeWatcher *tw, void *data)
|
|||||||
// Handle error message
|
// Handle error message
|
||||||
if (called_emsg > called_emsg_before && did_emsg) {
|
if (called_emsg > called_emsg_before && did_emsg) {
|
||||||
timer->emsg_count++;
|
timer->emsg_count++;
|
||||||
if (current_exception != NULL) {
|
if (did_throw) {
|
||||||
discard_current_exception();
|
discard_current_exception();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3010,7 +3010,7 @@ void ex_call(exarg_T *eap)
|
|||||||
|
|
||||||
// When inside :try we need to check for following "| catch" or "| endtry".
|
// When inside :try we need to check for following "| catch" or "| endtry".
|
||||||
// Not when there was an error, but do check if an exception was thrown.
|
// Not when there was an error, but do check if an exception was thrown.
|
||||||
if ((!aborting() || current_exception != NULL) && (!failed || eap->cstack->cs_trylevel > 0)) {
|
if ((!aborting() || did_throw) && (!failed || eap->cstack->cs_trylevel > 0)) {
|
||||||
// Check for trailing illegal characters and a following command.
|
// Check for trailing illegal characters and a following command.
|
||||||
if (!ends_excmd(*arg)) {
|
if (!ends_excmd(*arg)) {
|
||||||
if (!failed && !aborting()) {
|
if (!failed && !aborting()) {
|
||||||
|
@@ -134,6 +134,7 @@ struct dbg_stuff {
|
|||||||
char *vv_throwpoint;
|
char *vv_throwpoint;
|
||||||
int did_emsg;
|
int did_emsg;
|
||||||
int got_int;
|
int got_int;
|
||||||
|
bool did_throw;
|
||||||
int need_rethrow;
|
int need_rethrow;
|
||||||
int check_cstack;
|
int check_cstack;
|
||||||
except_T *current_exception;
|
except_T *current_exception;
|
||||||
@@ -165,6 +166,7 @@ static void save_dbg_stuff(struct dbg_stuff *dsp)
|
|||||||
// Necessary for debugging an inactive ":catch", ":finally", ":endtry".
|
// Necessary for debugging an inactive ":catch", ":finally", ":endtry".
|
||||||
dsp->did_emsg = did_emsg; did_emsg = false;
|
dsp->did_emsg = did_emsg; did_emsg = false;
|
||||||
dsp->got_int = got_int; got_int = false;
|
dsp->got_int = got_int; got_int = false;
|
||||||
|
dsp->did_throw = did_throw; did_throw = false;
|
||||||
dsp->need_rethrow = need_rethrow; need_rethrow = false;
|
dsp->need_rethrow = need_rethrow; need_rethrow = false;
|
||||||
dsp->check_cstack = check_cstack; check_cstack = false;
|
dsp->check_cstack = check_cstack; check_cstack = false;
|
||||||
dsp->current_exception = current_exception; current_exception = NULL;
|
dsp->current_exception = current_exception; current_exception = NULL;
|
||||||
@@ -180,6 +182,7 @@ static void restore_dbg_stuff(struct dbg_stuff *dsp)
|
|||||||
(void)v_throwpoint(dsp->vv_throwpoint);
|
(void)v_throwpoint(dsp->vv_throwpoint);
|
||||||
did_emsg = dsp->did_emsg;
|
did_emsg = dsp->did_emsg;
|
||||||
got_int = dsp->got_int;
|
got_int = dsp->got_int;
|
||||||
|
did_throw = dsp->did_throw;
|
||||||
need_rethrow = dsp->need_rethrow;
|
need_rethrow = dsp->need_rethrow;
|
||||||
check_cstack = dsp->check_cstack;
|
check_cstack = dsp->check_cstack;
|
||||||
current_exception = dsp->current_exception;
|
current_exception = dsp->current_exception;
|
||||||
@@ -397,7 +400,8 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags)
|
|||||||
|
|
||||||
initial_trylevel = trylevel;
|
initial_trylevel = trylevel;
|
||||||
|
|
||||||
current_exception = NULL;
|
// "did_throw" will be set to true when an exception is being thrown.
|
||||||
|
did_throw = false;
|
||||||
// "did_emsg" will be set to true when emsg() is used, in which case we
|
// "did_emsg" will be set to true when emsg() is used, in which case we
|
||||||
// cancel the whole command line, and any if/endif or loop.
|
// cancel the whole command line, and any if/endif or loop.
|
||||||
// If force_abort is set, we cancel everything.
|
// If force_abort is set, we cancel everything.
|
||||||
@@ -625,7 +629,7 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags)
|
|||||||
// not to use a cs_line[] from an entry that isn't a ":while"
|
// not to use a cs_line[] from an entry that isn't a ":while"
|
||||||
// or ":for": It would make "current_line" invalid and can
|
// or ":for": It would make "current_line" invalid and can
|
||||||
// cause a crash.
|
// cause a crash.
|
||||||
if (!did_emsg && !got_int && !current_exception
|
if (!did_emsg && !got_int && !did_throw
|
||||||
&& cstack.cs_idx >= 0
|
&& cstack.cs_idx >= 0
|
||||||
&& (cstack.cs_flags[cstack.cs_idx]
|
&& (cstack.cs_flags[cstack.cs_idx]
|
||||||
& (CSF_WHILE | CSF_FOR))
|
& (CSF_WHILE | CSF_FOR))
|
||||||
@@ -666,7 +670,7 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags)
|
|||||||
current_line = 0;
|
current_line = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A ":finally" makes did_emsg, got_int and current_exception pending for
|
// A ":finally" makes did_emsg, got_int and did_throw pending for
|
||||||
// being restored at the ":endtry". Reset them here and set the
|
// being restored at the ":endtry". Reset them here and set the
|
||||||
// ACTIVE and FINALLY flags, so that the finally clause gets executed.
|
// ACTIVE and FINALLY flags, so that the finally clause gets executed.
|
||||||
// This includes the case where a missing ":endif", ":endwhile" or
|
// This includes the case where a missing ":endif", ":endwhile" or
|
||||||
@@ -675,9 +679,8 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags)
|
|||||||
cstack.cs_lflags &= ~CSL_HAD_FINA;
|
cstack.cs_lflags &= ~CSL_HAD_FINA;
|
||||||
report_make_pending((cstack.cs_pending[cstack.cs_idx]
|
report_make_pending((cstack.cs_pending[cstack.cs_idx]
|
||||||
& (CSTP_ERROR | CSTP_INTERRUPT | CSTP_THROW)),
|
& (CSTP_ERROR | CSTP_INTERRUPT | CSTP_THROW)),
|
||||||
current_exception);
|
did_throw ? current_exception : NULL);
|
||||||
did_emsg = got_int = false;
|
did_emsg = got_int = did_throw = false;
|
||||||
current_exception = NULL;
|
|
||||||
cstack.cs_flags[cstack.cs_idx] |= CSF_ACTIVE | CSF_FINALLY;
|
cstack.cs_flags[cstack.cs_idx] |= CSF_ACTIVE | CSF_FINALLY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,7 +693,7 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags)
|
|||||||
// exception, cancel everything. If it is left normally, reset
|
// exception, cancel everything. If it is left normally, reset
|
||||||
// force_abort to get the non-EH compatible abortion behavior for
|
// force_abort to get the non-EH compatible abortion behavior for
|
||||||
// the rest of the script.
|
// the rest of the script.
|
||||||
if (trylevel == 0 && !did_emsg && !got_int && !current_exception) {
|
if (trylevel == 0 && !did_emsg && !got_int && !did_throw) {
|
||||||
force_abort = false;
|
force_abort = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,7 +707,7 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags)
|
|||||||
// - didn't get an error message or lines are not typed
|
// - didn't get an error message or lines are not typed
|
||||||
// - there is a command after '|', inside a :if, :while, :for or :try, or
|
// - there is a command after '|', inside a :if, :while, :for or :try, or
|
||||||
// looping for ":source" command or function call.
|
// looping for ":source" command or function call.
|
||||||
} while (!((got_int || (did_emsg && force_abort) || current_exception)
|
} while (!((got_int || (did_emsg && force_abort) || did_throw)
|
||||||
&& cstack.cs_trylevel == 0)
|
&& cstack.cs_trylevel == 0)
|
||||||
&& !(did_emsg
|
&& !(did_emsg
|
||||||
// Keep going when inside try/catch, so that the error can be
|
// Keep going when inside try/catch, so that the error can be
|
||||||
@@ -724,7 +727,7 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags)
|
|||||||
if (cstack.cs_idx >= 0) {
|
if (cstack.cs_idx >= 0) {
|
||||||
// If a sourced file or executed function ran to its end, report the
|
// If a sourced file or executed function ran to its end, report the
|
||||||
// unclosed conditional.
|
// unclosed conditional.
|
||||||
if (!got_int && !current_exception
|
if (!got_int && !did_throw
|
||||||
&& ((getline_equal(fgetline, cookie, getsourceline)
|
&& ((getline_equal(fgetline, cookie, getsourceline)
|
||||||
&& !source_finished(fgetline, cookie))
|
&& !source_finished(fgetline, cookie))
|
||||||
|| (getline_equal(fgetline, cookie, get_func_line)
|
|| (getline_equal(fgetline, cookie, get_func_line)
|
||||||
@@ -767,7 +770,8 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags)
|
|||||||
// conditional, discard the uncaught exception, disable the conversion
|
// conditional, discard the uncaught exception, disable the conversion
|
||||||
// of interrupts or errors to exceptions, and ensure that no more
|
// of interrupts or errors to exceptions, and ensure that no more
|
||||||
// commands are executed.
|
// commands are executed.
|
||||||
if (current_exception) {
|
if (did_throw) {
|
||||||
|
assert(current_exception != NULL);
|
||||||
char *p = NULL;
|
char *p = NULL;
|
||||||
msglist_T *messages = NULL;
|
msglist_T *messages = NULL;
|
||||||
msglist_T *next;
|
msglist_T *next;
|
||||||
@@ -829,14 +833,14 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags)
|
|||||||
// cstack belongs to the same function or, respectively, script file, it
|
// cstack belongs to the same function or, respectively, script file, it
|
||||||
// will have to be checked for finally clauses to be executed due to the
|
// will have to be checked for finally clauses to be executed due to the
|
||||||
// ":return" or ":finish". This is done in do_one_cmd().
|
// ":return" or ":finish". This is done in do_one_cmd().
|
||||||
if (current_exception) {
|
if (did_throw) {
|
||||||
need_rethrow = true;
|
need_rethrow = true;
|
||||||
}
|
}
|
||||||
if ((getline_equal(fgetline, cookie, getsourceline)
|
if ((getline_equal(fgetline, cookie, getsourceline)
|
||||||
&& ex_nesting_level > source_level(real_cookie))
|
&& ex_nesting_level > source_level(real_cookie))
|
||||||
|| (getline_equal(fgetline, cookie, get_func_line)
|
|| (getline_equal(fgetline, cookie, get_func_line)
|
||||||
&& ex_nesting_level > func_level(real_cookie) + 1)) {
|
&& ex_nesting_level > func_level(real_cookie) + 1)) {
|
||||||
if (!current_exception) {
|
if (!did_throw) {
|
||||||
check_cstack = true;
|
check_cstack = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1675,7 +1679,7 @@ static void profile_cmd(const exarg_T *eap, cstack_T *cstack, LineGetter fgetlin
|
|||||||
&& (!eap->skip || cstack->cs_idx == 0
|
&& (!eap->skip || cstack->cs_idx == 0
|
||||||
|| (cstack->cs_idx > 0
|
|| (cstack->cs_idx > 0
|
||||||
&& (cstack->cs_flags[cstack->cs_idx - 1] & CSF_ACTIVE)))) {
|
&& (cstack->cs_flags[cstack->cs_idx - 1] & CSF_ACTIVE)))) {
|
||||||
int skip = did_emsg || got_int || current_exception;
|
bool skip = did_emsg || got_int || did_throw;
|
||||||
|
|
||||||
if (eap->cmdidx == CMD_catch) {
|
if (eap->cmdidx == CMD_catch) {
|
||||||
skip = !skip && !(cstack->cs_idx >= 0
|
skip = !skip && !(cstack->cs_idx >= 0
|
||||||
@@ -1872,7 +1876,7 @@ static char *do_one_cmd(char **cmdlinep, int flags, cstack_T *cstack, LineGetter
|
|||||||
|
|
||||||
ea.skip = (did_emsg
|
ea.skip = (did_emsg
|
||||||
|| got_int
|
|| got_int
|
||||||
|| current_exception
|
|| did_throw
|
||||||
|| (cstack->cs_idx >= 0
|
|| (cstack->cs_idx >= 0
|
||||||
&& !(cstack->cs_flags[cstack->cs_idx] & CSF_ACTIVE)));
|
&& !(cstack->cs_flags[cstack->cs_idx] & CSF_ACTIVE)));
|
||||||
|
|
||||||
|
@@ -49,16 +49,15 @@
|
|||||||
// interrupt (got_int) under an active try conditional terminates the script
|
// interrupt (got_int) under an active try conditional terminates the script
|
||||||
// after the non-active finally clauses of all active try conditionals have been
|
// after the non-active finally clauses of all active try conditionals have been
|
||||||
// executed. Otherwise, errors and/or interrupts are converted into catchable
|
// executed. Otherwise, errors and/or interrupts are converted into catchable
|
||||||
// exceptions, which terminate the script only if not caught. For user
|
// exceptions (did_throw additionally set), which terminate the script only if
|
||||||
// exceptions, only current_exception is set. (Note: got_int can be set
|
// not caught. For user exceptions, only did_throw is set. (Note: got_int can
|
||||||
// asynchronously afterwards by a SIGINT, so current_exception && got_int is not
|
// be set asynchronously afterwards by a SIGINT, so did_throw && got_int is not
|
||||||
// a reliant test that the exception currently being thrown is an interrupt
|
// a reliant test that the exception currently being thrown is an interrupt
|
||||||
// exception. Similarly, did_emsg can be set afterwards on an error in an
|
// exception. Similarly, did_emsg can be set afterwards on an error in an
|
||||||
// (unskipped) conditional command inside an inactive conditional, so
|
// (unskipped) conditional command inside an inactive conditional, so did_throw
|
||||||
// current_exception && did_emsg is not a reliant test that the exception
|
// && did_emsg is not a reliant test that the exception currently being thrown
|
||||||
// currently being thrown is an error exception.) - The macros can be defined
|
// is an error exception.) - The macros can be defined as expressions checking
|
||||||
// as expressions checking for a variable that is allowed to be changed during
|
// for a variable that is allowed to be changed during execution of a script.
|
||||||
// execution of a script.
|
|
||||||
|
|
||||||
// Values used for the Vim release.
|
// Values used for the Vim release.
|
||||||
#define THROW_ON_ERROR true
|
#define THROW_ON_ERROR true
|
||||||
@@ -71,7 +70,7 @@
|
|||||||
#define CHECK_SKIP \
|
#define CHECK_SKIP \
|
||||||
(did_emsg \
|
(did_emsg \
|
||||||
|| got_int \
|
|| got_int \
|
||||||
|| current_exception \
|
|| did_throw \
|
||||||
|| (cstack->cs_idx > 0 \
|
|| (cstack->cs_idx > 0 \
|
||||||
&& !(cstack->cs_flags[cstack->cs_idx - 1] & CSF_ACTIVE)))
|
&& !(cstack->cs_flags[cstack->cs_idx - 1] & CSF_ACTIVE)))
|
||||||
|
|
||||||
@@ -104,7 +103,7 @@ static int cause_abort = false;
|
|||||||
/// value. "got_int" is also set by calling interrupt().
|
/// value. "got_int" is also set by calling interrupt().
|
||||||
int aborting(void)
|
int aborting(void)
|
||||||
{
|
{
|
||||||
return (did_emsg && force_abort) || got_int || current_exception;
|
return (did_emsg && force_abort) || got_int || did_throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The value of "force_abort" is temporarily reset by the first emsg() call
|
/// The value of "force_abort" is temporarily reset by the first emsg() call
|
||||||
@@ -186,7 +185,7 @@ bool cause_errthrow(const char *mesg, bool severe, bool *ignore)
|
|||||||
* currently throwing an exception, do nothing. The message text will
|
* currently throwing an exception, do nothing. The message text will
|
||||||
* then be stored to v:errmsg by emsg() without displaying it.
|
* then be stored to v:errmsg by emsg() without displaying it.
|
||||||
*/
|
*/
|
||||||
if (((trylevel == 0 && !cause_abort) || emsg_silent) && !current_exception) {
|
if (((trylevel == 0 && !cause_abort) || emsg_silent) && !did_throw) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +216,7 @@ bool cause_errthrow(const char *mesg, bool severe, bool *ignore)
|
|||||||
* exception currently being thrown to prevent it from being caught. Just
|
* exception currently being thrown to prevent it from being caught. Just
|
||||||
* execute finally clauses and terminate.
|
* execute finally clauses and terminate.
|
||||||
*/
|
*/
|
||||||
if (current_exception) {
|
if (did_throw) {
|
||||||
// When discarding an interrupt exception, reset got_int to prevent the
|
// When discarding an interrupt exception, reset got_int to prevent the
|
||||||
// same interrupt being converted to an exception again and discarding
|
// same interrupt being converted to an exception again and discarding
|
||||||
// the error exception we are about to throw here.
|
// the error exception we are about to throw here.
|
||||||
@@ -349,7 +348,7 @@ int do_intthrow(cstack_T *cstack)
|
|||||||
{
|
{
|
||||||
// If no interrupt occurred or no try conditional is active and no exception
|
// If no interrupt occurred or no try conditional is active and no exception
|
||||||
// is being thrown, do nothing (for compatibility of non-EH scripts).
|
// is being thrown, do nothing (for compatibility of non-EH scripts).
|
||||||
if (!got_int || (trylevel == 0 && !current_exception)) {
|
if (!got_int || (trylevel == 0 && !did_throw)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +357,7 @@ int do_intthrow(cstack_T *cstack)
|
|||||||
// The interrupt aborts everything except for executing finally clauses.
|
// The interrupt aborts everything except for executing finally clauses.
|
||||||
// Discard any user or error or interrupt exception currently being
|
// Discard any user or error or interrupt exception currently being
|
||||||
// thrown.
|
// thrown.
|
||||||
if (current_exception) {
|
if (did_throw) {
|
||||||
discard_current_exception();
|
discard_current_exception();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -369,7 +368,7 @@ int do_intthrow(cstack_T *cstack)
|
|||||||
// will be terminated then. - If an interrupt exception is already
|
// will be terminated then. - If an interrupt exception is already
|
||||||
// being thrown, do nothing.
|
// being thrown, do nothing.
|
||||||
|
|
||||||
if (current_exception) {
|
if (did_throw) {
|
||||||
if (current_exception->type == ET_INTERRUPT) {
|
if (current_exception->type == ET_INTERRUPT) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -596,6 +595,7 @@ void discard_current_exception(void)
|
|||||||
}
|
}
|
||||||
// Note: all globals manipulated here should be saved/restored in
|
// Note: all globals manipulated here should be saved/restored in
|
||||||
// try_enter/try_leave.
|
// try_enter/try_leave.
|
||||||
|
did_throw = false;
|
||||||
need_rethrow = false;
|
need_rethrow = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1237,6 +1237,8 @@ void do_throw(cstack_T *cstack)
|
|||||||
cstack->cs_flags[idx] &= ~CSF_ACTIVE;
|
cstack->cs_flags[idx] &= ~CSF_ACTIVE;
|
||||||
cstack->cs_exception[idx] = current_exception;
|
cstack->cs_exception[idx] = current_exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
did_throw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle ":try"
|
/// Handle ":try"
|
||||||
@@ -1345,7 +1347,7 @@ void ex_catch(exarg_T *eap)
|
|||||||
* corresponding try block never got active (because of an inactive
|
* corresponding try block never got active (because of an inactive
|
||||||
* surrounding conditional or after an error or interrupt or throw).
|
* surrounding conditional or after an error or interrupt or throw).
|
||||||
*/
|
*/
|
||||||
if (!current_exception || !(cstack->cs_flags[idx] & CSF_TRUE)) {
|
if (!did_throw || !(cstack->cs_flags[idx] & CSF_TRUE)) {
|
||||||
skip = true;
|
skip = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1406,10 +1408,10 @@ void ex_catch(exarg_T *eap)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (caught) {
|
if (caught) {
|
||||||
// Make this ":catch" clause active and reset did_emsg and got_int.
|
// Make this ":catch" clause active and reset did_emsg, got_int,
|
||||||
// Put the exception on the caught stack.
|
// and did_throw. Put the exception on the caught stack.
|
||||||
cstack->cs_flags[idx] |= CSF_ACTIVE | CSF_CAUGHT;
|
cstack->cs_flags[idx] |= CSF_ACTIVE | CSF_CAUGHT;
|
||||||
did_emsg = got_int = false;
|
did_emsg = got_int = did_throw = false;
|
||||||
catch_exception((except_T *)cstack->cs_exception[idx]);
|
catch_exception((except_T *)cstack->cs_exception[idx]);
|
||||||
// It's mandatory that the current exception is stored in the cstack
|
// It's mandatory that the current exception is stored in the cstack
|
||||||
// so that it can be discarded at the next ":catch", ":finally", or
|
// so that it can be discarded at the next ":catch", ":finally", or
|
||||||
@@ -1419,10 +1421,6 @@ void ex_catch(exarg_T *eap)
|
|||||||
if (cstack->cs_exception[cstack->cs_idx] != current_exception) {
|
if (cstack->cs_exception[cstack->cs_idx] != current_exception) {
|
||||||
internal_error("ex_catch()");
|
internal_error("ex_catch()");
|
||||||
}
|
}
|
||||||
// Discarding current_exceptions happens based on what is stored in
|
|
||||||
// cstack->cs_exception, *all* calls to discard_current_exception() are
|
|
||||||
// (and must be) guarded by current_exception check.
|
|
||||||
current_exception = NULL;
|
|
||||||
} else {
|
} else {
|
||||||
/*
|
/*
|
||||||
* If there is a preceding catch clause and it caught the exception,
|
* If there is a preceding catch clause and it caught the exception,
|
||||||
@@ -1477,14 +1475,12 @@ void ex_finally(exarg_T *eap)
|
|||||||
rewind_conditionals(cstack, idx, CSF_WHILE | CSF_FOR,
|
rewind_conditionals(cstack, idx, CSF_WHILE | CSF_FOR,
|
||||||
&cstack->cs_looplevel);
|
&cstack->cs_looplevel);
|
||||||
|
|
||||||
/*
|
// Don't do something when the corresponding try block never got active
|
||||||
* Don't do something when the corresponding try block never got active
|
// (because of an inactive surrounding conditional or after an error or
|
||||||
* (because of an inactive surrounding conditional or after an error or
|
// interrupt or throw) or for a ":finally" without ":try" or a multiple
|
||||||
* interrupt or throw) or for a ":finally" without ":try" or a multiple
|
// ":finally". After every other error (did_emsg or the conditional
|
||||||
* ":finally". After every other error (did_emsg or the conditional
|
// errors detected above) or after an interrupt (got_int) or an
|
||||||
* errors detected above) or after an interrupt (got_int) or an
|
// exception (did_throw), the finally clause must be executed.
|
||||||
* exception (current_exception), the finally clause must be executed.
|
|
||||||
*/
|
|
||||||
skip = !(cstack->cs_flags[cstack->cs_idx] & CSF_TRUE);
|
skip = !(cstack->cs_flags[cstack->cs_idx] & CSF_TRUE);
|
||||||
|
|
||||||
if (!skip) {
|
if (!skip) {
|
||||||
@@ -1509,22 +1505,20 @@ void ex_finally(exarg_T *eap)
|
|||||||
*/
|
*/
|
||||||
cleanup_conditionals(cstack, CSF_TRY, false);
|
cleanup_conditionals(cstack, CSF_TRY, false);
|
||||||
|
|
||||||
/*
|
// Make did_emsg, got_int, did_throw pending. If set, they overrule
|
||||||
* Make did_emsg, got_int, current_exception pending. If set, they
|
// a pending ":continue", ":break", ":return", or ":finish". Then
|
||||||
* overrule a pending ":continue", ":break", ":return", or ":finish".
|
// we have particularly to discard a pending return value (as done
|
||||||
* Then we have particularly to discard a pending return value (as done
|
// by the call to cleanup_conditionals() above when did_emsg or
|
||||||
* by the call to cleanup_conditionals() above when did_emsg or
|
// got_int is set). The pending values are restored by the
|
||||||
* got_int is set). The pending values are restored by the
|
// ":endtry", except if there is a new error, interrupt, exception,
|
||||||
* ":endtry", except if there is a new error, interrupt, exception,
|
// ":continue", ":break", ":return", or ":finish" in the following
|
||||||
* ":continue", ":break", ":return", or ":finish" in the following
|
// finally clause. A missing ":endwhile", ":endfor" or ":endif"
|
||||||
* finally clause. A missing ":endwhile", ":endfor" or ":endif"
|
// detected here is treated as if did_emsg and did_throw had
|
||||||
* detected here is treated as if did_emsg and current_exception had
|
// already been set, respectively in case that the error is not
|
||||||
* already been set, respectively in case that the error is not
|
// converted to an exception, did_throw had already been unset.
|
||||||
* converted to an exception, current_exception had already been unset.
|
// We must not set did_emsg here since that would suppress the
|
||||||
* We must not set did_emsg here since that would suppress the
|
// error message.
|
||||||
* error message.
|
if (pending == CSTP_ERROR || did_emsg || got_int || did_throw) {
|
||||||
*/
|
|
||||||
if (pending == CSTP_ERROR || did_emsg || got_int || current_exception) {
|
|
||||||
if (cstack->cs_pending[cstack->cs_idx] == CSTP_RETURN) {
|
if (cstack->cs_pending[cstack->cs_idx] == CSTP_RETURN) {
|
||||||
report_discard_pending(CSTP_RETURN,
|
report_discard_pending(CSTP_RETURN,
|
||||||
cstack->cs_rettv[cstack->cs_idx]);
|
cstack->cs_rettv[cstack->cs_idx]);
|
||||||
@@ -1533,7 +1527,7 @@ void ex_finally(exarg_T *eap)
|
|||||||
if (pending == CSTP_ERROR && !did_emsg) {
|
if (pending == CSTP_ERROR && !did_emsg) {
|
||||||
pending |= (THROW_ON_ERROR ? CSTP_THROW : 0);
|
pending |= (THROW_ON_ERROR ? CSTP_THROW : 0);
|
||||||
} else {
|
} else {
|
||||||
pending |= (current_exception ? CSTP_THROW : 0);
|
pending |= (did_throw ? CSTP_THROW : 0);
|
||||||
}
|
}
|
||||||
pending |= did_emsg ? CSTP_ERROR : 0;
|
pending |= did_emsg ? CSTP_ERROR : 0;
|
||||||
pending |= got_int ? CSTP_INTERRUPT : 0;
|
pending |= got_int ? CSTP_INTERRUPT : 0;
|
||||||
@@ -1547,19 +1541,16 @@ void ex_finally(exarg_T *eap)
|
|||||||
// exception. When emsg() is called for a missing ":endif" or
|
// exception. When emsg() is called for a missing ":endif" or
|
||||||
// a missing ":endwhile"/":endfor" detected here, the
|
// a missing ":endwhile"/":endfor" detected here, the
|
||||||
// exception will be discarded.
|
// exception will be discarded.
|
||||||
if (current_exception
|
if (did_throw && cstack->cs_exception[cstack->cs_idx] != current_exception) {
|
||||||
&& cstack->cs_exception[cstack->cs_idx] != current_exception) {
|
|
||||||
internal_error("ex_finally()");
|
internal_error("ex_finally()");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// Set CSL_HAD_FINA, so do_cmdline() will reset did_emsg,
|
||||||
* Set CSL_HAD_FINA, so do_cmdline() will reset did_emsg,
|
// got_int, and did_throw and make the finally clause active.
|
||||||
* got_int, and current_exception and make the finally clause active.
|
// This will happen after emsg() has been called for a missing
|
||||||
* This will happen after emsg() has been called for a missing
|
// ":endif" or a missing ":endwhile"/":endfor" detected here, so
|
||||||
* ":endif" or a missing ":endwhile"/":endfor" detected here, so
|
// that the following finally clause will be executed even then.
|
||||||
* that the following finally clause will be executed even then.
|
|
||||||
*/
|
|
||||||
cstack->cs_lflags |= CSL_HAD_FINA;
|
cstack->cs_lflags |= CSL_HAD_FINA;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1587,8 +1578,7 @@ void ex_endtry(exarg_T *eap)
|
|||||||
// made inactive by a ":continue", ":break", ":return", or ":finish" in
|
// made inactive by a ":continue", ":break", ":return", or ":finish" in
|
||||||
// the finally clause. The latter case need not be tested since then
|
// the finally clause. The latter case need not be tested since then
|
||||||
// anything pending has already been discarded.
|
// anything pending has already been discarded.
|
||||||
bool skip = did_emsg || got_int || current_exception
|
bool skip = did_emsg || got_int || did_throw || !(cstack->cs_flags[cstack->cs_idx] & CSF_TRUE);
|
||||||
|| !(cstack->cs_flags[cstack->cs_idx] & CSF_TRUE);
|
|
||||||
|
|
||||||
if (!(cstack->cs_flags[cstack->cs_idx] & CSF_TRY)) {
|
if (!(cstack->cs_flags[cstack->cs_idx] & CSF_TRY)) {
|
||||||
eap->errmsg = get_end_emsg(cstack);
|
eap->errmsg = get_end_emsg(cstack);
|
||||||
@@ -1602,14 +1592,12 @@ void ex_endtry(exarg_T *eap)
|
|||||||
&cstack->cs_looplevel);
|
&cstack->cs_looplevel);
|
||||||
skip = true;
|
skip = true;
|
||||||
|
|
||||||
/*
|
// If an exception is being thrown, discard it to prevent it from
|
||||||
* If an exception is being thrown, discard it to prevent it from
|
// being rethrown at the end of this function. It would be
|
||||||
* being rethrown at the end of this function. It would be
|
// discarded by the error message, anyway. Resets did_throw.
|
||||||
* discarded by the error message, anyway. Resets current_exception.
|
// This does not affect the script termination due to the error
|
||||||
* This does not affect the script termination due to the error
|
// since "trylevel" is decremented after emsg() has been called.
|
||||||
* since "trylevel" is decremented after emsg() has been called.
|
if (did_throw) {
|
||||||
*/
|
|
||||||
if (current_exception) {
|
|
||||||
discard_current_exception();
|
discard_current_exception();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1624,7 +1612,7 @@ void ex_endtry(exarg_T *eap)
|
|||||||
* a finally clause, we need to rethrow it after closing the try
|
* a finally clause, we need to rethrow it after closing the try
|
||||||
* conditional.
|
* conditional.
|
||||||
*/
|
*/
|
||||||
if (current_exception
|
if (did_throw
|
||||||
&& (cstack->cs_flags[idx] & CSF_TRUE)
|
&& (cstack->cs_flags[idx] & CSF_TRUE)
|
||||||
&& !(cstack->cs_flags[idx] & CSF_FINALLY)) {
|
&& !(cstack->cs_flags[idx] & CSF_FINALLY)) {
|
||||||
rethrow = true;
|
rethrow = true;
|
||||||
@@ -1648,10 +1636,10 @@ void ex_endtry(exarg_T *eap)
|
|||||||
if (got_int) {
|
if (got_int) {
|
||||||
skip = true;
|
skip = true;
|
||||||
(void)do_intthrow(cstack);
|
(void)do_intthrow(cstack);
|
||||||
// The do_intthrow() call may have reset current_exception or
|
// The do_intthrow() call may have reset did_throw or
|
||||||
// cstack->cs_pending[idx].
|
// cstack->cs_pending[idx].
|
||||||
rethrow = false;
|
rethrow = false;
|
||||||
if (current_exception && !(cstack->cs_flags[idx] & CSF_FINALLY)) {
|
if (did_throw && !(cstack->cs_flags[idx] & CSF_FINALLY)) {
|
||||||
rethrow = true;
|
rethrow = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1721,7 +1709,7 @@ void ex_endtry(exarg_T *eap)
|
|||||||
// When the finally clause was entered due to an error,
|
// When the finally clause was entered due to an error,
|
||||||
// interrupt or throw (as opposed to a ":continue", ":break",
|
// interrupt or throw (as opposed to a ":continue", ":break",
|
||||||
// ":return", or ":finish"), restore the pending values of
|
// ":return", or ":finish"), restore the pending values of
|
||||||
// did_emsg, got_int, and current_exception. This is skipped, if there
|
// did_emsg, got_int, and did_throw. This is skipped, if there
|
||||||
// was a new error, interrupt, throw, ":continue", ":break",
|
// was a new error, interrupt, throw, ":continue", ":break",
|
||||||
// ":return", or ":finish". in the finally clause.
|
// ":return", or ":finish". in the finally clause.
|
||||||
default:
|
default:
|
||||||
@@ -1768,24 +1756,22 @@ void enter_cleanup(cleanup_T *csp)
|
|||||||
{
|
{
|
||||||
int pending = CSTP_NONE;
|
int pending = CSTP_NONE;
|
||||||
|
|
||||||
/*
|
// Postpone did_emsg, got_int, did_throw. The pending values will be
|
||||||
* Postpone did_emsg, got_int, current_exception. The pending values will be
|
// restored by leave_cleanup() except if there was an aborting error,
|
||||||
* restored by leave_cleanup() except if there was an aborting error,
|
// interrupt, or uncaught exception after this function ends.
|
||||||
* interrupt, or uncaught exception after this function ends.
|
if (did_emsg || got_int || did_throw || need_rethrow) {
|
||||||
*/
|
|
||||||
if (did_emsg || got_int || current_exception || need_rethrow) {
|
|
||||||
csp->pending = (did_emsg ? CSTP_ERROR : 0)
|
csp->pending = (did_emsg ? CSTP_ERROR : 0)
|
||||||
| (got_int ? CSTP_INTERRUPT : 0)
|
| (got_int ? CSTP_INTERRUPT : 0)
|
||||||
| (current_exception ? CSTP_THROW : 0)
|
| (did_throw ? CSTP_THROW : 0)
|
||||||
| (need_rethrow ? CSTP_THROW : 0);
|
| (need_rethrow ? CSTP_THROW : 0);
|
||||||
|
|
||||||
// If we are currently throwing an exception, save it as well. On an error
|
// If we are currently throwing an exception (did_throw), save it as
|
||||||
// not yet converted to an exception, update "force_abort" and reset
|
// well. On an error not yet converted to an exception, update
|
||||||
// "cause_abort" (as do_errthrow() would do). This is needed for the
|
// "force_abort" and reset "cause_abort" (as do_errthrow() would do).
|
||||||
// do_cmdline() call that is going to be made for autocommand execution. We
|
// This is needed for the do_cmdline() call that is going to be made
|
||||||
// need not save *msg_list because there is an extra instance for every call
|
// for autocommand execution. We need not save *msg_list because
|
||||||
// of do_cmdline(), anyway.
|
// there is an extra instance for every call of do_cmdline(), anyway.
|
||||||
if (current_exception || need_rethrow) {
|
if (did_throw || need_rethrow) {
|
||||||
csp->exception = current_exception;
|
csp->exception = current_exception;
|
||||||
current_exception = NULL;
|
current_exception = NULL;
|
||||||
} else {
|
} else {
|
||||||
@@ -1795,8 +1781,7 @@ void enter_cleanup(cleanup_T *csp)
|
|||||||
cause_abort = false;
|
cause_abort = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
did_emsg = got_int = need_rethrow = false;
|
did_emsg = got_int = did_throw = need_rethrow = false;
|
||||||
current_exception = NULL;
|
|
||||||
|
|
||||||
// Report if required by the 'verbose' option or when debugging.
|
// Report if required by the 'verbose' option or when debugging.
|
||||||
report_make_pending(pending, csp->exception);
|
report_make_pending(pending, csp->exception);
|
||||||
@@ -1866,7 +1851,7 @@ void leave_cleanup(cleanup_T *csp)
|
|||||||
force_abort = false;
|
force_abort = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore the pending values of did_emsg, got_int, and current_exception.
|
// Restore the pending values of did_emsg, got_int, and did_throw.
|
||||||
if (pending & CSTP_ERROR) {
|
if (pending & CSTP_ERROR) {
|
||||||
did_emsg = true;
|
did_emsg = true;
|
||||||
}
|
}
|
||||||
@@ -1874,7 +1859,7 @@ void leave_cleanup(cleanup_T *csp)
|
|||||||
got_int = true;
|
got_int = true;
|
||||||
}
|
}
|
||||||
if (pending & CSTP_THROW) {
|
if (pending & CSTP_THROW) {
|
||||||
need_rethrow = true; // current_exception will be set by do_one_cmd()
|
need_rethrow = true; // did_throw will be set by do_one_cmd()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report if required by the 'verbose' option or when debugging.
|
// Report if required by the 'verbose' option or when debugging.
|
||||||
|
@@ -241,9 +241,13 @@ EXTERN int do_profiling INIT(= PROF_NONE); ///< PROF_ values
|
|||||||
|
|
||||||
/// Exception currently being thrown. Used to pass an exception to a different
|
/// Exception currently being thrown. Used to pass an exception to a different
|
||||||
/// cstack. Also used for discarding an exception before it is caught or made
|
/// cstack. Also used for discarding an exception before it is caught or made
|
||||||
/// pending.
|
/// pending. Only valid when did_throw is true.
|
||||||
EXTERN except_T *current_exception;
|
EXTERN except_T *current_exception;
|
||||||
|
|
||||||
|
/// An exception is being thrown. Reset when the exception is caught or as
|
||||||
|
/// long as it is pending in a finally clause.
|
||||||
|
EXTERN bool did_throw INIT(= false);
|
||||||
|
|
||||||
/// Set when a throw that cannot be handled in do_cmdline() must be propagated
|
/// Set when a throw that cannot be handled in do_cmdline() must be propagated
|
||||||
/// to the cstack of the previously called do_cmdline().
|
/// to the cstack of the previously called do_cmdline().
|
||||||
EXTERN bool need_rethrow INIT(= false);
|
EXTERN bool need_rethrow INIT(= false);
|
||||||
|
@@ -1027,7 +1027,7 @@ int nlua_call(lua_State *lstate)
|
|||||||
// TODO(bfredl): this should be simplified in error handling refactor
|
// TODO(bfredl): this should be simplified in error handling refactor
|
||||||
force_abort = false;
|
force_abort = false;
|
||||||
suppress_errthrow = false;
|
suppress_errthrow = false;
|
||||||
current_exception = NULL;
|
did_throw = false;
|
||||||
did_emsg = false;
|
did_emsg = false;
|
||||||
|
|
||||||
try_start();
|
try_start();
|
||||||
|
@@ -6,6 +6,12 @@ local funcs = helpers.funcs
|
|||||||
local nvim_prog = helpers.nvim_prog
|
local nvim_prog = helpers.nvim_prog
|
||||||
local matches = helpers.matches
|
local matches = helpers.matches
|
||||||
|
|
||||||
|
clear()
|
||||||
|
if funcs.executable('man') == 0 then
|
||||||
|
pending('missing "man" command', function() end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
describe(':Man', function()
|
describe(':Man', function()
|
||||||
before_each(function()
|
before_each(function()
|
||||||
clear()
|
clear()
|
||||||
|
@@ -219,3 +219,31 @@ it('Error when if/for/while/try/function is nested too deep',function()
|
|||||||
feed(':call Test5()<CR>')
|
feed(':call Test5()<CR>')
|
||||||
screen:expect({any = 'E1058: '})
|
screen:expect({any = 'E1058: '})
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
describe("uncaught exception", function()
|
||||||
|
before_each(clear)
|
||||||
|
after_each(function()
|
||||||
|
os.remove('throw1.vim')
|
||||||
|
os.remove('throw2.vim')
|
||||||
|
os.remove('throw3.vim')
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('is not forgotten #13490', function()
|
||||||
|
command('autocmd BufWinEnter * throw "i am error"')
|
||||||
|
eq('i am error', exc_exec('try | new | endtry'))
|
||||||
|
|
||||||
|
-- Like Vim, throwing here aborts the processing of the script, but does not stop :runtime!
|
||||||
|
-- from processing the others.
|
||||||
|
-- Only the first thrown exception should be rethrown from the :try below, though.
|
||||||
|
for i = 1, 3 do
|
||||||
|
write_file('throw' .. i .. '.vim', ([[
|
||||||
|
let result ..= '%d'
|
||||||
|
throw 'throw%d'
|
||||||
|
let result ..= 'X'
|
||||||
|
]]):format(i, i))
|
||||||
|
end
|
||||||
|
command('set runtimepath+=. | let result = ""')
|
||||||
|
eq('throw1', exc_exec('try | runtime! throw*.vim | endtry'))
|
||||||
|
eq('123', eval('result'))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
Reference in New Issue
Block a user