aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/builtin.txt60
-rw-r--r--runtime/doc/userfunc.txt60
-rw-r--r--src/nvim/eval/funcs.c56
-rw-r--r--src/nvim/eval/typval_defs.h1
-rw-r--r--src/nvim/eval/userfunc.c272
-rw-r--r--src/nvim/ex_cmds.lua12
-rw-r--r--src/nvim/ex_eval.c2
-rw-r--r--src/nvim/fileio.c4
-rw-r--r--src/nvim/globals.h2
-rw-r--r--src/nvim/log.c2
-rw-r--r--src/nvim/main.c4
-rw-r--r--src/nvim/memline.c2
-rw-r--r--src/nvim/os/fs.c11
-rw-r--r--src/nvim/shada.c2
-rw-r--r--src/nvim/undo.c2
-rw-r--r--test/old/testdir/test_autochdir.vim19
-rw-r--r--test/old/testdir/test_autocmd.vim17
-rw-r--r--test/old/testdir/test_eval_stuff.vim64
-rw-r--r--test/old/testdir/test_quickfix.vim22
-rw-r--r--test/old/testdir/test_user_func.vim102
-rw-r--r--test/old/testdir/test_writefile.vim50
-rw-r--r--test/unit/os/fs_spec.lua51
22 files changed, 643 insertions, 174 deletions
diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index cb05718ab9..15ccfd9b92 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -5572,8 +5572,24 @@ mkdir({name} [, {flags} [, {prot}]])
When {flags} is present it must be a string. An empty string
has no effect.
- If {flags} is "p" then intermediate directories are created as
- necessary.
+ If {flags} contains "p" then intermediate directories are
+ created as necessary.
+
+ If {flags} contains "D" then {name} is deleted at the end of
+ the current function, as with: >
+ defer delete({name}, 'd')
+<
+ If {flags} contains "R" then {name} is deleted recursively at
+ the end of the current function, as with: >
+ defer delete({name}, 'rf')
+< Note that when {name} has more than one part and "p" is used
+ some directories may already exist. Only the first one that
+ is created and what it contains is scheduled to be deleted.
+ E.g. when using: >
+ call mkdir('subdir/tmp/autoload', 'pR')
+< and "subdir" already exists then "subdir/tmp" will be
+ scheduled for deletion, like with: >
+ defer delete('subdir/tmp', 'rf')
If {prot} is given it is used to set the protection bits of
the new directory. The default is 0o755 (rwxr-xr-x: r/w for
@@ -9507,31 +9523,43 @@ writefile({object}, {fname} [, {flags}])
When {object} is a |List| write it to file {fname}. Each list
item is separated with a NL. Each list item must be a String
or Number.
- When {flags} contains "b" then binary mode is used: There will
- not be a NL after the last list item. An empty item at the
- end does cause the last line in the file to end in a NL.
+ All NL characters are replaced with a NUL character.
+ Inserting CR characters needs to be done before passing {list}
+ to writefile().
When {object} is a |Blob| write the bytes to file {fname}
- unmodified.
+ unmodified, also when binary mode is not specified.
- When {flags} contains "a" then append mode is used, lines are
- appended to the file: >
+ {flags} must be a String. These characters are recognized:
+
+ 'b' Binary mode is used: There will not be a NL after the
+ last list item. An empty item at the end does cause the
+ last line in the file to end in a NL.
+
+ 'a' Append mode is used, lines are appended to the file: >
:call writefile(["foo"], "event.log", "a")
:call writefile(["bar"], "event.log", "a")
<
- When {flags} contains "S" fsync() call is not used, with "s"
- it is used, 'fsync' option applies by default. No fsync()
- means that writefile() will finish faster, but writes may be
- left in OS buffers and not yet written to disk. Such changes
- will disappear if system crashes before OS does writing.
+ 'D' Delete the file when the current function ends. This
+ works like: >
+ :defer delete({fname})
+< Fails when not in a function. Also see |:defer|.
+
+ 's' fsync() is called after writing the file. This flushes
+ the file to disk, if possible. This takes more time but
+ avoids losing the file if the system crashes.
+
+ 'S' fsync() is not called, even when 'fsync' is set.
+
+ When {flags} does not contain "S" or "s" then fsync() is
+ called if the 'fsync' option is set.
- All NL characters are replaced with a NUL character.
- Inserting CR characters needs to be done before passing {list}
- to writefile().
An existing file is overwritten, if possible.
+
When the write fails -1 is returned, otherwise 0. There is an
error message if the file can't be created or when writing
fails.
+
Also see |readfile()|.
To copy a file byte for byte: >
:let fl = readfile("foo", "b")
diff --git a/runtime/doc/userfunc.txt b/runtime/doc/userfunc.txt
index ce6f2fc2e9..db0127df95 100644
--- a/runtime/doc/userfunc.txt
+++ b/runtime/doc/userfunc.txt
@@ -350,10 +350,68 @@ A function can also be called as part of evaluating an expression or when it
is used as a method: >
let x = GetList()
let y = GetList()->Filter()
+<
+==============================================================================
+3. Cleaning up in a function ~
+ *:defer*
+:defer {func}({args}) Call {func} when the current function is done.
+ {args} are evaluated here.
+
+Quite often a command in a function has a global effect, which must be undone
+when the function finishes. Handling this in all kinds of situations can be a
+hassle. Especially when an unexpected error is encountered. This can be done
+with `try` / `finally` blocks, but this gets complicated when there is more
+than one.
+
+A much simpler solution is using `defer`. It schedules a function call when
+the function is returning, no matter if there is an error. Example: >
+ func Filter(text) abort
+ call writefile(a:text, 'Tempfile')
+ call system('filter < Tempfile > Outfile')
+ call Handle('Outfile')
+ call delete('Tempfile')
+ call delete('Outfile')
+ endfunc
+
+Here 'Tempfile' and 'Outfile' will not be deleted if something causes the
+function to abort. `:defer` can be used to avoid that: >
+ func Filter(text) abort
+ call writefile(a:text, 'Tempfile')
+ defer delete('Tempfile')
+ defer delete('Outfile')
+ call system('filter < Tempfile > Outfile')
+ call Handle('Outfile')
+ endfunc
+
+Note that deleting "Outfile" is scheduled before calling `system()`, since it
+can be created even when `system()` fails.
+
+The deferred functions are called in reverse order, the last one added is
+executed first. A useless example: >
+ func Useless() abort
+ for s in range(3)
+ defer execute('echomsg "number ' .. s .. '"')
+ endfor
+ endfunc
+
+Now `:messages` shows:
+ number 2
+ number 1
+ number 0
+
+Any return value of the deferred function is discarded. The function cannot
+be followed by anything, such as "->func" or ".member". Currently `:defer
+GetArg()->TheFunc()` does not work, it may work in a later version.
+
+Errors are reported but do not cause aborting execution of deferred functions.
+
+No range is accepted. The function can be a partial with extra arguments, but
+not with a dictionary. *E1300*
==============================================================================
-3. Automatically loading functions ~
+
+4. Automatically loading functions ~
*autoload-functions*
When using many or large functions, it's possible to automatically define them
only when they are used. There are two methods: with an autocommand and with
diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c
index 99e511a7a4..f53b283c79 100644
--- a/src/nvim/eval/funcs.c
+++ b/src/nvim/eval/funcs.c
@@ -4892,6 +4892,9 @@ static void f_mkdir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
*path_tail_with_sep((char *)dir) = NUL;
}
+ bool defer = false;
+ bool defer_recurse = false;
+ char *created = NULL;
if (argvars[1].v_type != VAR_UNKNOWN) {
if (argvars[2].v_type != VAR_UNKNOWN) {
prot = (int)tv_get_number_chk(&argvars[2], NULL);
@@ -4899,9 +4902,17 @@ static void f_mkdir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
return;
}
}
- if (strcmp(tv_get_string(&argvars[1]), "p") == 0) {
+ const char *arg2 = tv_get_string(&argvars[1]);
+ defer = vim_strchr(arg2, 'D') != NULL;
+ defer_recurse = vim_strchr(arg2, 'R') != NULL;
+ if ((defer || defer_recurse) && !can_add_defer()) {
+ return;
+ }
+
+ if (vim_strchr(arg2, 'p') != NULL) {
char *failed_dir;
- int ret = os_mkdir_recurse(dir, prot, &failed_dir);
+ int ret = os_mkdir_recurse(dir, prot, &failed_dir,
+ defer || defer_recurse ? &created : NULL);
if (ret != 0) {
semsg(_(e_mkdir), failed_dir, os_strerror(ret));
xfree(failed_dir);
@@ -4909,10 +4920,27 @@ static void f_mkdir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
return;
}
rettv->vval.v_number = OK;
- return;
}
}
- rettv->vval.v_number = vim_mkdir_emsg(dir, prot);
+ if (rettv->vval.v_number == FAIL) {
+ rettv->vval.v_number = vim_mkdir_emsg(dir, prot);
+ }
+
+ // Handle "D" and "R": deferred deletion of the created directory.
+ if (rettv->vval.v_number == OK
+ && created == NULL && (defer || defer_recurse)) {
+ created = FullName_save(dir, false);
+ }
+ if (created != NULL) {
+ typval_T tv[2];
+ tv[0].v_type = VAR_STRING;
+ tv[0].v_lock = VAR_UNLOCKED;
+ tv[0].vval.v_string = created;
+ tv[1].v_type = VAR_STRING;
+ tv[1].v_lock = VAR_UNLOCKED;
+ tv[1].vval.v_string = xstrdup(defer_recurse ? "rf" : "d");
+ add_defer("delete", 2, tv);
+ }
}
/// "mode()" function
@@ -9296,6 +9324,7 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
bool binary = false;
bool append = false;
+ bool defer = false;
bool do_fsync = !!p_fs;
bool mkdir_p = false;
if (argvars[2].v_type != VAR_UNKNOWN) {
@@ -9309,6 +9338,8 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
binary = true; break;
case 'a':
append = true; break;
+ case 'D':
+ defer = true; break;
case 's':
do_fsync = true; break;
case 'S':
@@ -9328,6 +9359,11 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
if (fname == NULL) {
return;
}
+
+ if (defer && !can_add_defer()) {
+ return;
+ }
+
FileDescriptor fp;
int error;
if (*fname == NUL) {
@@ -9336,9 +9372,17 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
((append ? kFileAppend : kFileTruncate)
| (mkdir_p ? kFileMkDir : kFileCreate)
| kFileCreate), 0666)) != 0) {
- semsg(_("E482: Can't open file %s for writing: %s"),
- fname, os_strerror(error));
+ semsg(_("E482: Can't open file %s for writing: %s"), fname, os_strerror(error));
} else {
+ if (defer) {
+ typval_T tv = {
+ .v_type = VAR_STRING,
+ .v_lock = VAR_UNLOCKED,
+ .vval.v_string = FullName_save(fname, false),
+ };
+ add_defer("delete", 1, &tv);
+ }
+
bool write_ok;
if (argvars[0].v_type == VAR_BLOB) {
write_ok = write_blob(&fp, argvars[0].vval.v_blob);
diff --git a/src/nvim/eval/typval_defs.h b/src/nvim/eval/typval_defs.h
index 80432271b0..517e59f3c2 100644
--- a/src/nvim/eval/typval_defs.h
+++ b/src/nvim/eval/typval_defs.h
@@ -299,6 +299,7 @@ struct funccall_S {
linenr_T breakpoint; ///< Next line with breakpoint or zero.
int dbg_tick; ///< debug_tick when breakpoint was set.
int level; ///< Top nesting level of executed function.
+ garray_T fc_defer; ///< Functions to be called on return.
proftime_T prof_child; ///< Time spent in a child.
funccall_T *caller; ///< Calling function or NULL; or next funccal in
///< list pointed to by previous_funccal.
diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c
index a348588106..f91962ac09 100644
--- a/src/nvim/eval/userfunc.c
+++ b/src/nvim/eval/userfunc.c
@@ -53,6 +53,13 @@
# include "eval/userfunc.c.generated.h"
#endif
+/// structure used as item in "fc_defer"
+typedef struct {
+ char *dr_name; ///< function name, allocated
+ typval_T dr_argvars[MAX_FUNC_ARGS + 1];
+ int dr_argcount;
+} defer_T;
+
static hashtab_T func_hashtab;
// Used by get_func_tv()
@@ -74,6 +81,8 @@ static const char e_no_white_space_allowed_before_str_str[]
= N_("E1068: No white space allowed before '%s': %s");
static const char e_missing_heredoc_end_marker_str[]
= N_("E1145: Missing heredoc end marker: %s");
+static const char e_cannot_use_partial_with_dictionary_for_defer[]
+ = N_("E1300: Cannot use a partial with dictionary for :defer");
void func_init(void)
{
@@ -469,45 +478,63 @@ void emsg_funcname(const char *errmsg, const char *name)
}
}
-/// Allocate a variable for the result of a function.
-///
-/// @param name name of the function
-/// @param len length of "name" or -1 to use strlen()
-/// @param arg argument, pointing to the '('
-/// @param funcexe various values
-///
-/// @return OK or FAIL.
-int get_func_tv(const char *name, int len, typval_T *rettv, char **arg, evalarg_T *const evalarg,
- funcexe_T *funcexe)
+/// Get function arguments at "*arg" and advance it.
+/// Return them in "*argvars[MAX_FUNC_ARGS + 1]" and the count in "argcount".
+/// On failure FAIL is returned but the "argvars[argcount]" are still set.
+static int get_func_arguments(char **arg, evalarg_T *const evalarg, int partial_argc,
+ typval_T *argvars, int *argcount)
{
- char *argp;
+ char *argp = *arg;
int ret = OK;
- typval_T argvars[MAX_FUNC_ARGS + 1]; // vars for arguments
- int argcount = 0; // number of arguments found
- const bool evaluate = evalarg == NULL ? false : (evalarg->eval_flags & EVAL_EVALUATE);
// Get the arguments.
- argp = *arg;
- while (argcount < MAX_FUNC_ARGS
- - (funcexe->fe_partial == NULL ? 0 : funcexe->fe_partial->pt_argc)) {
+ while (*argcount < MAX_FUNC_ARGS - partial_argc) {
argp = skipwhite(argp + 1); // skip the '(' or ','
+
if (*argp == ')' || *argp == ',' || *argp == NUL) {
break;
}
- if (eval1(&argp, &argvars[argcount], evalarg) == FAIL) {
+ if (eval1(&argp, &argvars[*argcount], evalarg) == FAIL) {
ret = FAIL;
break;
}
- argcount++;
+ (*argcount)++;
if (*argp != ',') {
break;
}
}
+
+ argp = skipwhite(argp);
if (*argp == ')') {
argp++;
} else {
ret = FAIL;
}
+ *arg = argp;
+ return ret;
+}
+
+/// Call a function and put the result in "rettv".
+///
+/// @param name name of the function
+/// @param len length of "name" or -1 to use strlen()
+/// @param arg argument, pointing to the '('
+/// @param funcexe various values
+///
+/// @return OK or FAIL.
+int get_func_tv(const char *name, int len, typval_T *rettv, char **arg, evalarg_T *const evalarg,
+ funcexe_T *funcexe)
+{
+ typval_T argvars[MAX_FUNC_ARGS + 1]; // vars for arguments
+ int argcount = 0; // number of arguments found
+ const bool evaluate = evalarg == NULL ? false : (evalarg->eval_flags & EVAL_EVALUATE);
+
+ char *argp = *arg;
+ int ret = get_func_arguments(&argp, evalarg,
+ (funcexe->fe_partial == NULL
+ ? 0
+ : funcexe->fe_partial->pt_argc),
+ argvars, &argcount);
if (ret == OK) {
int i = 0;
@@ -1148,6 +1175,9 @@ void call_user_func(ufunc_T *fp, int argcount, typval_T *argvars, typval_T *rett
DOCMD_NOWAIT|DOCMD_VERBOSE|DOCMD_REPEAT);
}
+ // Invoke functions added with ":defer".
+ handle_defer_one(current_funccal);
+
RedrawingDisabled--;
// when the function was aborted because of an error, return -1
@@ -1544,7 +1574,7 @@ int call_func(const char *funcname, int len, typval_T *rettv, int argcount_in, t
int argv_base = 0;
partial_T *partial = funcexe->fe_partial;
- // Initialize rettv so that it is safe for caller to invoke clear_tv(rettv)
+ // Initialize rettv so that it is safe for caller to invoke tv_clear(rettv)
// even when call_func() returns FAIL.
rettv->v_type = VAR_UNKNOWN;
@@ -3033,7 +3063,156 @@ void ex_return(exarg_T *eap)
clear_evalarg(&evalarg, eap);
}
+/// Lower level implementation of "call". Only called when not skipping.
+static int ex_call_inner(exarg_T *eap, char *name, char **arg, char *startarg,
+ const funcexe_T *const funcexe_init, evalarg_T *const evalarg)
+{
+ bool doesrange;
+ bool failed = false;
+
+ for (linenr_T lnum = eap->line1; lnum <= eap->line2; lnum++) {
+ if (eap->addr_count > 0) {
+ if (lnum > curbuf->b_ml.ml_line_count) {
+ // If the function deleted lines or switched to another buffer
+ // the line number may become invalid.
+ emsg(_(e_invrange));
+ break;
+ }
+ curwin->w_cursor.lnum = lnum;
+ curwin->w_cursor.col = 0;
+ curwin->w_cursor.coladd = 0;
+ }
+ *arg = startarg;
+
+ funcexe_T funcexe = *funcexe_init;
+ funcexe.fe_doesrange = &doesrange;
+ typval_T rettv;
+ rettv.v_type = VAR_UNKNOWN; // tv_clear() uses this
+ if (get_func_tv(name, -1, &rettv, arg, evalarg, &funcexe) == FAIL) {
+ failed = true;
+ break;
+ }
+
+ // Handle a function returning a Funcref, Dictionary or List.
+ if (handle_subscript((const char **)arg, &rettv, &EVALARG_EVALUATE, true) == FAIL) {
+ failed = true;
+ break;
+ }
+
+ tv_clear(&rettv);
+ if (doesrange) {
+ break;
+ }
+
+ // Stop when immediately aborting on error, or when an interrupt
+ // occurred or an exception was thrown but not caught.
+ // get_func_tv() returned OK, so that the check for trailing
+ // characters below is executed.
+ if (aborting()) {
+ break;
+ }
+ }
+
+ return failed;
+}
+
+/// Core part of ":defer func(arg)". "arg" points to the "(" and is advanced.
+///
+/// @return FAIL or OK.
+static int ex_defer_inner(char *name, char **arg, const partial_T *const partial,
+ evalarg_T *const evalarg)
+{
+ typval_T argvars[MAX_FUNC_ARGS + 1]; // vars for arguments
+ int partial_argc = 0; // number of partial arguments
+ int argcount = 0; // number of arguments found
+
+ if (current_funccal == NULL) {
+ semsg(_(e_str_not_inside_function), "defer");
+ return FAIL;
+ }
+ if (partial != NULL) {
+ if (partial->pt_dict != NULL) {
+ emsg(_(e_cannot_use_partial_with_dictionary_for_defer));
+ return FAIL;
+ }
+ if (partial->pt_argc > 0) {
+ partial_argc = partial->pt_argc;
+ for (int i = 0; i < partial_argc; i++) {
+ tv_copy(&partial->pt_argv[i], &argvars[i]);
+ }
+ }
+ }
+ int r = get_func_arguments(arg, evalarg, false, argvars + partial_argc, &argcount);
+ argcount += partial_argc;
+ if (r == FAIL) {
+ while (--argcount >= 0) {
+ tv_clear(&argvars[argcount]);
+ }
+ return FAIL;
+ }
+ add_defer(name, argcount, argvars);
+ return OK;
+}
+
+/// Return true if currently inside a function call.
+/// Give an error message and return FALSE when not.
+bool can_add_defer(void)
+{
+ if (get_current_funccal() == NULL) {
+ semsg(_(e_str_not_inside_function), "defer");
+ return false;
+ }
+ return true;
+}
+
+/// Add a deferred call for "name" with arguments "argvars[argcount]".
+/// Consumes "argvars[]".
+/// Caller must check that current_funccal is not NULL.
+void add_defer(char *name, int argcount_arg, typval_T *argvars)
+{
+ char *saved_name = xstrdup(name);
+ int argcount = argcount_arg;
+
+ if (current_funccal->fc_defer.ga_itemsize == 0) {
+ ga_init(&current_funccal->fc_defer, sizeof(defer_T), 10);
+ }
+ defer_T *dr = GA_APPEND_VIA_PTR(defer_T, &current_funccal->fc_defer);
+ dr->dr_name = saved_name;
+ dr->dr_argcount = argcount;
+ while (argcount > 0) {
+ argcount--;
+ dr->dr_argvars[argcount] = argvars[argcount];
+ }
+}
+
+/// Invoked after a function has finished: invoke ":defer" functions.
+static void handle_defer_one(funccall_T *funccal)
+{
+ for (int idx = funccal->fc_defer.ga_len - 1; idx >= 0; idx--) {
+ defer_T *dr = ((defer_T *)funccal->fc_defer.ga_data) + idx;
+ funcexe_T funcexe = { .fe_evaluate = true };
+ typval_T rettv;
+ rettv.v_type = VAR_UNKNOWN; // tv_clear() uses this
+ call_func(dr->dr_name, -1, &rettv, dr->dr_argcount, dr->dr_argvars, &funcexe);
+ tv_clear(&rettv);
+ xfree(dr->dr_name);
+ for (int i = dr->dr_argcount - 1; i >= 0; i--) {
+ tv_clear(&dr->dr_argvars[i]);
+ }
+ }
+ ga_clear(&funccal->fc_defer);
+}
+
+/// Called when exiting: call all defer functions.
+void invoke_all_defer(void)
+{
+ for (funccall_T *funccal = current_funccal; funccal != NULL; funccal = funccal->caller) {
+ handle_defer_one(funccal);
+ }
+}
+
/// ":1,25call func(arg1, arg2)" function call.
+/// ":defer func(arg1, arg2)" deferred function call.
void ex_call(exarg_T *eap)
{
char *arg = eap->arg;
@@ -3041,9 +3220,6 @@ void ex_call(exarg_T *eap)
char *name;
char *tofree;
int len;
- typval_T rettv;
- linenr_T lnum;
- bool doesrange;
bool failed = false;
funcdict_T fudi;
partial_T *partial = NULL;
@@ -3051,6 +3227,7 @@ void ex_call(exarg_T *eap)
fill_evalarg_from_eap(&evalarg, eap, eap->skip);
if (eap->skip) {
+ typval_T rettv;
// trans_function_name() doesn't work well when skipping, use eval0()
// instead to skip to any following command, e.g. for:
// :if 0 | call dict.foo().bar() | endif.
@@ -3089,59 +3266,24 @@ void ex_call(exarg_T *eap)
// Skip white space to allow ":call func ()". Not good, but required for
// backward compatibility.
startarg = skipwhite(arg);
- rettv.v_type = VAR_UNKNOWN; // tv_clear() uses this.
if (*startarg != '(') {
semsg(_(e_missingparen), eap->arg);
goto end;
}
- lnum = eap->line1;
- for (; lnum <= eap->line2; lnum++) {
- if (eap->addr_count > 0) { // -V560
- if (lnum > curbuf->b_ml.ml_line_count) {
- // If the function deleted lines or switched to another buffer
- // the line number may become invalid.
- emsg(_(e_invrange));
- break;
- }
- curwin->w_cursor.lnum = lnum;
- curwin->w_cursor.col = 0;
- curwin->w_cursor.coladd = 0;
- }
+ if (eap->cmdidx == CMD_defer) {
arg = startarg;
-
+ failed = ex_defer_inner(name, &arg, partial, &evalarg) == FAIL;
+ } else {
funcexe_T funcexe = FUNCEXE_INIT;
- funcexe.fe_firstline = eap->line1;
- funcexe.fe_lastline = eap->line2;
- funcexe.fe_doesrange = &doesrange;
- funcexe.fe_evaluate = true;
funcexe.fe_partial = partial;
funcexe.fe_selfdict = fudi.fd_dict;
+ funcexe.fe_firstline = eap->line1;
+ funcexe.fe_lastline = eap->line2;
funcexe.fe_found_var = found_var;
- if (get_func_tv(name, -1, &rettv, &arg, &evalarg, &funcexe) == FAIL) {
- failed = true;
- break;
- }
-
- // Handle a function returning a Funcref, Dictionary or List.
- if (handle_subscript((const char **)&arg, &rettv, &EVALARG_EVALUATE, true) == FAIL) {
- failed = true;
- break;
- }
-
- tv_clear(&rettv);
- if (doesrange) {
- break;
- }
-
- // Stop when immediately aborting on error, or when an interrupt
- // occurred or an exception was thrown but not caught.
- // get_func_tv() returned OK, so that the check for trailing
- // characters below is executed.
- if (aborting()) {
- break;
- }
+ funcexe.fe_evaluate = true;
+ failed = ex_call_inner(eap, name, &arg, startarg, &funcexe, &evalarg);
}
// When inside :try we need to check for following "| catch" or "| endtry".
diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua
index be6299db0e..845ea4bb15 100644
--- a/src/nvim/ex_cmds.lua
+++ b/src/nvim/ex_cmds.lua
@@ -715,6 +715,18 @@ module.cmds = {
func='ex_debuggreedy',
},
{
+ command='def',
+ flags=bit.bor(EXTRA, BANG, SBOXOK, CMDWIN, LOCK_OK),
+ addr_type='ADDR_NONE',
+ func='ex_ni',
+ },
+ {
+ command='defer',
+ flags=bit.bor(NEEDARG, EXTRA, NOTRLCOM, CMDWIN, LOCK_OK),
+ addr_type='ADDR_NONE',
+ func='ex_call',
+ },
+ {
command='delcommand',
flags=bit.bor(BANG, NEEDARG, WORD1, TRLBAR, CMDWIN, LOCK_OK),
addr_type='ADDR_NONE',
diff --git a/src/nvim/ex_eval.c b/src/nvim/ex_eval.c
index 5404ae6731..12d1f3d9bd 100644
--- a/src/nvim/ex_eval.c
+++ b/src/nvim/ex_eval.c
@@ -1966,7 +1966,7 @@ void rewind_conditionals(cstack_T *cstack, int idx, int cond_type, int *cond_lev
/// Handle ":endfunction" when not after a ":function"
void ex_endfunction(exarg_T *eap)
{
- emsg(_("E193: :endfunction not inside a function"));
+ semsg(_(e_str_not_inside_function), ":endfunction");
}
/// @return true if the string "p" looks like a ":while" or ":for" command.
diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c
index d4725ccd86..fa2f72932f 100644
--- a/src/nvim/fileio.c
+++ b/src/nvim/fileio.c
@@ -2536,7 +2536,7 @@ static int buf_write_make_backup(char *fname, bool append, FileInfo *file_info_o
if (*dirp == NUL && !os_isdir(IObuff)) {
int ret;
char *failed_dir;
- if ((ret = os_mkdir_recurse(IObuff, 0755, &failed_dir)) != 0) {
+ if ((ret = os_mkdir_recurse(IObuff, 0755, &failed_dir, NULL)) != 0) {
semsg(_("E303: Unable to create directory \"%s\" for backup file: %s"),
failed_dir, os_strerror(ret));
xfree(failed_dir);
@@ -2679,7 +2679,7 @@ nobackup:
if (*dirp == NUL && !os_isdir(IObuff)) {
int ret;
char *failed_dir;
- if ((ret = os_mkdir_recurse(IObuff, 0755, &failed_dir)) != 0) {
+ if ((ret = os_mkdir_recurse(IObuff, 0755, &failed_dir, NULL)) != 0) {
semsg(_("E303: Unable to create directory \"%s\" for backup file: %s"),
failed_dir, os_strerror(ret));
xfree(failed_dir);
diff --git a/src/nvim/globals.h b/src/nvim/globals.h
index 3c31d07e33..0d4f02eaee 100644
--- a/src/nvim/globals.h
+++ b/src/nvim/globals.h
@@ -986,6 +986,8 @@ EXTERN const char e_maxmempat[] INIT(= N_("E363: pattern uses more memory than '
EXTERN const char e_emptybuf[] INIT(= N_("E749: empty buffer"));
EXTERN const char e_nobufnr[] INIT(= N_("E86: Buffer %" PRId64 " does not exist"));
+EXTERN const char e_str_not_inside_function[] INIT(= N_("E193: %s not inside a function"));
+
EXTERN const char e_invalpat[] INIT(= N_("E682: Invalid search pattern or delimiter"));
EXTERN const char e_bufloaded[] INIT(= N_("E139: File is loaded in another buffer"));
EXTERN const char e_notset[] INIT(= N_("E764: Option '%s' is not set"));
diff --git a/src/nvim/log.c b/src/nvim/log.c
index 77eeb09fec..4de0c4d88c 100644
--- a/src/nvim/log.c
+++ b/src/nvim/log.c
@@ -76,7 +76,7 @@ static void log_path_init(void)
char *failed_dir = NULL;
bool log_dir_failure = false;
if (!os_isdir(loghome)) {
- log_dir_failure = (os_mkdir_recurse(loghome, 0700, &failed_dir) != 0);
+ log_dir_failure = (os_mkdir_recurse(loghome, 0700, &failed_dir, NULL) != 0);
}
XFREE_CLEAR(loghome);
// Invalid $NVIM_LOG_FILE or failed to expand; fall back to default.
diff --git a/src/nvim/main.c b/src/nvim/main.c
index 0ef1fc9391..698c2dcc4f 100644
--- a/src/nvim/main.c
+++ b/src/nvim/main.c
@@ -30,6 +30,7 @@
#include "nvim/eval.h"
#include "nvim/eval/typval.h"
#include "nvim/eval/typval_defs.h"
+#include "nvim/eval/userfunc.h"
#include "nvim/event/multiqueue.h"
#include "nvim/event/stream.h"
#include "nvim/ex_cmds.h"
@@ -693,6 +694,9 @@ void getout(int exitval)
// Position the cursor on the last screen line, below all the text
ui_cursor_goto(Rows - 1, 0);
+ // Invoked all deferred functions in the function stack.
+ invoke_all_defer();
+
// Optionally print hashtable efficiency.
hash_debug_results();
diff --git a/src/nvim/memline.c b/src/nvim/memline.c
index 18d5e75a53..0c38f18739 100644
--- a/src/nvim/memline.c
+++ b/src/nvim/memline.c
@@ -3442,7 +3442,7 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_
} else if (!*found_existing_dir && **dirp == NUL) {
int ret;
char *failed_dir;
- if ((ret = os_mkdir_recurse(dir_name, 0755, &failed_dir)) != 0) {
+ if ((ret = os_mkdir_recurse(dir_name, 0755, &failed_dir, NULL)) != 0) {
semsg(_("E303: Unable to create directory \"%s\" for swap file, "
"recovery impossible: %s"),
failed_dir, os_strerror(ret));
diff --git a/src/nvim/os/fs.c b/src/nvim/os/fs.c
index cb51e81005..872d9c9314 100644
--- a/src/nvim/os/fs.c
+++ b/src/nvim/os/fs.c
@@ -937,10 +937,13 @@ int os_mkdir(const char *path, int32_t mode)
/// the name of the directory which os_mkdir_recurse
/// failed to create. I.e. it will contain dir or any
/// of the higher level directories.
+/// @param[out] created Set to the full name of the first created directory.
+/// It will be NULL until that happens.
///
/// @return `0` for success, libuv error code for failure.
-int os_mkdir_recurse(const char *const dir, int32_t mode, char **const failed_dir)
- FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
+int os_mkdir_recurse(const char *const dir, int32_t mode, char **const failed_dir,
+ char **const created)
+ FUNC_ATTR_NONNULL_ARG(1, 3) FUNC_ATTR_WARN_UNUSED_RESULT
{
// Get end of directory name in "dir".
// We're done when it's "/" or "c:/".
@@ -975,6 +978,8 @@ int os_mkdir_recurse(const char *const dir, int32_t mode, char **const failed_di
if ((ret = os_mkdir(curdir, mode)) != 0) {
*failed_dir = curdir;
return ret;
+ } else if (created != NULL && *created == NULL) {
+ *created = FullName_save(curdir, false);
}
}
xfree(curdir);
@@ -1002,7 +1007,7 @@ int os_file_mkdir(char *fname, int32_t mode)
*tail = NUL;
int r;
char *failed_dir;
- if (((r = os_mkdir_recurse(fname, mode, &failed_dir)) < 0)) {
+ if (((r = os_mkdir_recurse(fname, mode, &failed_dir, NULL)) < 0)) {
semsg(_(e_mkdir), failed_dir, os_strerror(r));
xfree(failed_dir);
}
diff --git a/src/nvim/shada.c b/src/nvim/shada.c
index 78499922bf..fcb8a15cde 100644
--- a/src/nvim/shada.c
+++ b/src/nvim/shada.c
@@ -3042,7 +3042,7 @@ shada_write_file_nomerge: {}
if (!os_isdir(fname)) {
int ret;
char *failed_dir;
- if ((ret = os_mkdir_recurse(fname, 0700, &failed_dir)) != 0) {
+ if ((ret = os_mkdir_recurse(fname, 0700, &failed_dir, NULL)) != 0) {
semsg(_(SERR "Failed to create directory %s "
"for writing ShaDa file: %s"),
failed_dir, os_strerror(ret));
diff --git a/src/nvim/undo.c b/src/nvim/undo.c
index 132c84231f..7eb0d390fc 100644
--- a/src/nvim/undo.c
+++ b/src/nvim/undo.c
@@ -705,7 +705,7 @@ char *u_get_undo_file_name(const char *const buf_ffname, const bool reading)
// Last directory in the list does not exist, create it.
int ret;
char *failed_dir;
- if ((ret = os_mkdir_recurse(dir_name, 0755, &failed_dir)) != 0) {
+ if ((ret = os_mkdir_recurse(dir_name, 0755, &failed_dir, NULL)) != 0) {
semsg(_("E5003: Unable to create directory \"%s\" for undo file: %s"),
failed_dir, os_strerror(ret));
xfree(failed_dir);
diff --git a/test/old/testdir/test_autochdir.vim b/test/old/testdir/test_autochdir.vim
index a8810047a0..652ce8b794 100644
--- a/test/old/testdir/test_autochdir.vim
+++ b/test/old/testdir/test_autochdir.vim
@@ -30,9 +30,9 @@ func Test_set_filename_other_window()
CheckFunction test_autochdir
let cwd = getcwd()
call test_autochdir()
- call mkdir('Xa')
- call mkdir('Xb')
- call mkdir('Xc')
+ call mkdir('Xa', 'R')
+ call mkdir('Xb', 'R')
+ call mkdir('Xc', 'R')
try
args Xa/aaa.txt Xb/bbb.txt
set acd
@@ -44,9 +44,6 @@ func Test_set_filename_other_window()
finally
set noacd
call chdir(cwd)
- call delete('Xa', 'rf')
- call delete('Xb', 'rf')
- call delete('Xc', 'rf')
bwipe! aaa.txt
bwipe! bbb.txt
bwipe! ccc.txt
@@ -59,10 +56,10 @@ func Test_acd_win_execute()
set acd
call test_autochdir()
- call mkdir('Xfile')
+ call mkdir('XacdDir', 'R')
let winid = win_getid()
- new Xfile/file
- call assert_match('testdir.Xfile$', getcwd())
+ new XacdDir/file
+ call assert_match('testdir.XacdDir$', getcwd())
cd ..
call assert_match('testdir$', getcwd())
call win_execute(winid, 'echo')
@@ -71,7 +68,6 @@ func Test_acd_win_execute()
bwipe!
set noacd
call chdir(cwd)
- call delete('Xfile', 'rf')
endfunc
func Test_verbose_pwd()
@@ -82,7 +78,7 @@ func Test_verbose_pwd()
edit global.txt
call assert_match('\[global\].*testdir$', execute('verbose pwd'))
- call mkdir('Xautodir')
+ call mkdir('Xautodir', 'R')
split Xautodir/local.txt
lcd Xautodir
call assert_match('\[window\].*testdir[/\\]Xautodir', execute('verbose pwd'))
@@ -116,7 +112,6 @@ func Test_verbose_pwd()
bwipe!
call chdir(cwd)
- call delete('Xautodir', 'rf')
endfunc
func Test_multibyte()
diff --git a/test/old/testdir/test_autocmd.vim b/test/old/testdir/test_autocmd.vim
index 6d7f1649b3..ec671369f5 100644
--- a/test/old/testdir/test_autocmd.vim
+++ b/test/old/testdir/test_autocmd.vim
@@ -918,14 +918,13 @@ func Test_BufEnter()
call assert_equal('++', g:val)
" Also get BufEnter when editing a directory
- call mkdir('Xdir')
- split Xdir
+ call mkdir('Xbufenterdir', 'D')
+ split Xbufenterdir
call assert_equal('+++', g:val)
" On MS-Windows we can't edit the directory, make sure we wipe the right
" buffer.
- bwipe! Xdir
- call delete('Xdir', 'd')
+ bwipe! Xbufenterdir
au! BufEnter
" Editing a "nofile" buffer doesn't read the file but does trigger BufEnter
@@ -2186,11 +2185,10 @@ func Test_BufWriteCmd()
new
file Xbufwritecmd
set buftype=acwrite
- call mkdir('Xbufwritecmd')
+ call mkdir('Xbufwritecmd', 'D')
write
" BufWriteCmd should be triggered even if a directory has the same name
call assert_equal(1, g:written)
- call delete('Xbufwritecmd', 'd')
unlet g:written
au! BufWriteCmd
bwipe!
@@ -2947,16 +2945,15 @@ func Test_throw_in_BufWritePre()
endfunc
func Test_autocmd_in_try_block()
- call mkdir('Xdir')
+ call mkdir('Xintrydir', 'R')
au BufEnter * let g:fname = expand('%')
try
- edit Xdir/
+ edit Xintrydir/
endtry
- call assert_match('Xdir', g:fname)
+ call assert_match('Xintrydir', g:fname)
unlet g:fname
au! BufEnter
- call delete('Xdir', 'rf')
endfunc
func Test_autocmd_CmdWinEnter()
diff --git a/test/old/testdir/test_eval_stuff.vim b/test/old/testdir/test_eval_stuff.vim
index 7acc91c17b..20eb873326 100644
--- a/test/old/testdir/test_eval_stuff.vim
+++ b/test/old/testdir/test_eval_stuff.vim
@@ -36,12 +36,70 @@ func Test_mkdir_p()
endtry
" 'p' doesn't suppress real errors
call writefile([], 'Xfile')
- call assert_fails('call mkdir("Xfile", "p")', 'E739')
+ call assert_fails('call mkdir("Xfile", "p")', 'E739:')
call delete('Xfile')
call delete('Xmkdir', 'rf')
call assert_equal(0, mkdir(v:_null_string))
- call assert_fails('call mkdir([])', 'E730')
- call assert_fails('call mkdir("abc", [], [])', 'E745')
+ call assert_fails('call mkdir([])', 'E730:')
+ call assert_fails('call mkdir("abc", [], [])', 'E745:')
+endfunc
+
+func DoMkdirDel(name)
+ call mkdir(a:name, 'pD')
+ call assert_true(isdirectory(a:name))
+endfunc
+
+func DoMkdirDelAddFile(name)
+ call mkdir(a:name, 'pD')
+ call assert_true(isdirectory(a:name))
+ call writefile(['text'], a:name .. '/file')
+endfunc
+
+func DoMkdirDelRec(name)
+ call mkdir(a:name, 'pR')
+ call assert_true(isdirectory(a:name))
+endfunc
+
+func DoMkdirDelRecAddFile(name)
+ call mkdir(a:name, 'pR')
+ call assert_true(isdirectory(a:name))
+ call writefile(['text'], a:name .. '/file')
+endfunc
+
+func Test_mkdir_defer_del()
+ " Xtopdir/tmp is created thus deleted, not Xtopdir itself
+ call mkdir('Xtopdir', 'R')
+ call DoMkdirDel('Xtopdir/tmp')
+ call assert_true(isdirectory('Xtopdir'))
+ call assert_false(isdirectory('Xtopdir/tmp'))
+
+ " Deletion fails because "tmp" contains "sub"
+ call DoMkdirDel('Xtopdir/tmp/sub')
+ call assert_true(isdirectory('Xtopdir'))
+ call assert_true(isdirectory('Xtopdir/tmp'))
+ call delete('Xtopdir/tmp', 'rf')
+
+ " Deletion fails because "tmp" contains "file"
+ call DoMkdirDelAddFile('Xtopdir/tmp')
+ call assert_true(isdirectory('Xtopdir'))
+ call assert_true(isdirectory('Xtopdir/tmp'))
+ call assert_true(filereadable('Xtopdir/tmp/file'))
+ call delete('Xtopdir/tmp', 'rf')
+
+ " Xtopdir/tmp is created thus deleted, not Xtopdir itself
+ call DoMkdirDelRec('Xtopdir/tmp')
+ call assert_true(isdirectory('Xtopdir'))
+ call assert_false(isdirectory('Xtopdir/tmp'))
+
+ " Deletion works even though "tmp" contains "sub"
+ call DoMkdirDelRec('Xtopdir/tmp/sub')
+ call assert_true(isdirectory('Xtopdir'))
+ call assert_false(isdirectory('Xtopdir/tmp'))
+
+ " Deletion works even though "tmp" contains "file"
+ call DoMkdirDelRecAddFile('Xtopdir/tmp')
+ call assert_true(isdirectory('Xtopdir'))
+ call assert_false(isdirectory('Xtopdir/tmp'))
endfunc
func Test_line_continuation()
diff --git a/test/old/testdir/test_quickfix.vim b/test/old/testdir/test_quickfix.vim
index 838c4b1c15..f720b6e42d 100644
--- a/test/old/testdir/test_quickfix.vim
+++ b/test/old/testdir/test_quickfix.vim
@@ -6250,28 +6250,6 @@ func Test_very_long_error_line()
call setqflist([], 'f')
endfunc
-" The test depends on deferred delete and string interpolation, which haven't
-" been ported, so override it with a rewrite that doesn't use these features.
-func! Test_very_long_error_line()
- let msg = repeat('abcdefghijklmn', 146)
- let emsg = 'Xlonglines.c:1:' . msg
- call writefile([msg, emsg], 'Xerror')
- cfile Xerror
- call delete('Xerror')
- cwindow
- call assert_equal('|| ' .. msg, getline(1))
- call assert_equal('Xlonglines.c|1| ' .. msg, getline(2))
- cclose
-
- let l = execute('clist!')->split("\n")
- call assert_equal([' 1: ' .. msg, ' 2 Xlonglines.c:1: ' .. msg], l)
-
- let l = execute('cc')->split("\n")
- call assert_equal(['(2 of 2): ' .. msg], l)
-
- call setqflist([], 'f')
-endfunc
-
" In the quickfix window, spaces at the beginning of an informational line
" should not be removed but should be removed from an error line.
func Test_info_line_with_space()
diff --git a/test/old/testdir/test_user_func.vim b/test/old/testdir/test_user_func.vim
index f475803ce1..4bb4078a1c 100644
--- a/test/old/testdir/test_user_func.vim
+++ b/test/old/testdir/test_user_func.vim
@@ -532,4 +532,106 @@ func Test_funcdef_alloc_failure()
bw!
endfunc
+func AddDefer(arg1, ...)
+ call extend(g:deferred, [a:arg1])
+ if a:0 == 1
+ call extend(g:deferred, [a:1])
+ endif
+endfunc
+
+func WithDeferTwo()
+ call extend(g:deferred, ['in Two'])
+ for nr in range(3)
+ defer AddDefer('Two' .. nr)
+ endfor
+ call extend(g:deferred, ['end Two'])
+endfunc
+
+func WithDeferOne()
+ call extend(g:deferred, ['in One'])
+ call writefile(['text'], 'Xfuncdefer')
+ defer delete('Xfuncdefer')
+ defer AddDefer('One')
+ call WithDeferTwo()
+ call extend(g:deferred, ['end One'])
+endfunc
+
+func WithPartialDefer()
+ call extend(g:deferred, ['in Partial'])
+ let Part = funcref('AddDefer', ['arg1'])
+ defer Part("arg2")
+ call extend(g:deferred, ['end Partial'])
+endfunc
+
+func Test_defer()
+ let g:deferred = []
+ call WithDeferOne()
+
+ call assert_equal(['in One', 'in Two', 'end Two', 'Two2', 'Two1', 'Two0', 'end One', 'One'], g:deferred)
+ unlet g:deferred
+
+ call assert_equal('', glob('Xfuncdefer'))
+
+ call assert_fails('defer delete("Xfuncdefer")->Another()', 'E488:')
+ call assert_fails('defer delete("Xfuncdefer").member', 'E488:')
+
+ let g:deferred = []
+ call WithPartialDefer()
+ call assert_equal(['in Partial', 'end Partial', 'arg1', 'arg2'], g:deferred)
+ unlet g:deferred
+
+ let Part = funcref('AddDefer', ['arg1'], {})
+ call assert_fails('defer Part("arg2")', 'E1300:')
+endfunc
+
+func DeferLevelTwo()
+ call writefile(['text'], 'XDeleteTwo', 'D')
+ throw 'someerror'
+endfunc
+
+" def DeferLevelOne()
+func DeferLevelOne()
+ call writefile(['text'], 'XDeleteOne', 'D')
+ call g:DeferLevelTwo()
+" enddef
+endfunc
+
+func Test_defer_throw()
+ let caught = 'no'
+ try
+ call DeferLevelOne()
+ catch /someerror/
+ let caught = 'yes'
+ endtry
+ call assert_equal('yes', caught)
+ call assert_false(filereadable('XDeleteOne'))
+ call assert_false(filereadable('XDeleteTwo'))
+endfunc
+
+func Test_defer_quitall()
+ let lines =<< trim END
+ " vim9script
+ func DeferLevelTwo()
+ call writefile(['text'], 'XQuitallTwo', 'D')
+ qa!
+ endfunc
+
+ " def DeferLevelOne()
+ func DeferLevelOne()
+ call writefile(['text'], 'XQuitallOne', 'D')
+ call DeferLevelTwo()
+ " enddef
+ endfunc
+
+ " DeferLevelOne()
+ call DeferLevelOne()
+ END
+ call writefile(lines, 'XdeferQuitall', 'D')
+ let res = system(GetVimCommandClean() .. ' -X -S XdeferQuitall')
+ call assert_equal(0, v:shell_error)
+ call assert_false(filereadable('XQuitallOne'))
+ call assert_false(filereadable('XQuitallTwo'))
+endfunc
+
+
" vim: shiftwidth=2 sts=2 expandtab
diff --git a/test/old/testdir/test_writefile.vim b/test/old/testdir/test_writefile.vim
index 6019cee193..312d45e18f 100644
--- a/test/old/testdir/test_writefile.vim
+++ b/test/old/testdir/test_writefile.vim
@@ -924,19 +924,49 @@ endfunc
" Test for ':write ++bin' and ':write ++nobin'
func Test_write_binary_file()
" create a file without an eol/eof character
- call writefile(0z616161, 'Xfile1', 'b')
- new Xfile1
- write ++bin Xfile2
- write ++nobin Xfile3
- call assert_equal(0z616161, readblob('Xfile2'))
+ call writefile(0z616161, 'Xwbfile1', 'b')
+ new Xwbfile1
+ write ++bin Xwbfile2
+ write ++nobin Xwbfile3
+ call assert_equal(0z616161, readblob('Xwbfile2'))
if has('win32')
- call assert_equal(0z6161610D.0A, readblob('Xfile3'))
+ call assert_equal(0z6161610D.0A, readblob('Xwbfile3'))
else
- call assert_equal(0z6161610A, readblob('Xfile3'))
+ call assert_equal(0z6161610A, readblob('Xwbfile3'))
endif
- call delete('Xfile1')
- call delete('Xfile2')
- call delete('Xfile3')
+ call delete('Xwbfile1')
+ call delete('Xwbfile2')
+ call delete('Xwbfile3')
+endfunc
+
+func DoWriteDefer()
+ call writefile(['some text'], 'XdeferDelete', 'D')
+ call assert_equal(['some text'], readfile('XdeferDelete'))
+endfunc
+
+" def DefWriteDefer()
+" writefile(['some text'], 'XdefdeferDelete', 'D')
+" assert_equal(['some text'], readfile('XdefdeferDelete'))
+" enddef
+
+func Test_write_with_deferred_delete()
+ call DoWriteDefer()
+ call assert_equal('', glob('XdeferDelete'))
+ " call DefWriteDefer()
+ " call assert_equal('', glob('XdefdeferDelete'))
+endfunc
+
+func DoWriteFile()
+ call writefile(['text'], 'Xthefile', 'D')
+ cd ..
+endfunc
+
+func Test_write_defer_delete_chdir()
+ let dir = getcwd()
+ call DoWriteFile()
+ call assert_notequal(dir, getcwd())
+ call chdir(dir)
+ call assert_equal('', glob('Xthefile'))
endfunc
" Check that buffer is written before triggering QuitPre
diff --git a/test/unit/os/fs_spec.lua b/test/unit/os/fs_spec.lua
index 95a12f5b17..3a40e97755 100644
--- a/test/unit/os/fs_spec.lua
+++ b/test/unit/os/fs_spec.lua
@@ -17,6 +17,7 @@ local OK = helpers.OK
local FAIL = helpers.FAIL
local NULL = helpers.NULL
local mkdir = helpers.mkdir
+local endswith = helpers.endswith
local NODE_NORMAL = 0
local NODE_WRITABLE = 1
@@ -748,12 +749,17 @@ describe('fs.c', function()
local function os_mkdir_recurse(path, mode)
local failed_str = ffi.new('char *[1]', {nil})
- local ret = fs.os_mkdir_recurse(path, mode, failed_str)
- local str = failed_str[0]
- if str ~= nil then
- str = ffi.string(str)
+ local created_str = ffi.new('char *[1]', {nil})
+ local ret = fs.os_mkdir_recurse(path, mode, failed_str, created_str)
+ local failed_dir = failed_str[0]
+ if failed_dir ~= nil then
+ failed_dir = ffi.string(failed_dir)
end
- return ret, str
+ local created_dir = created_str[0]
+ if created_dir ~= nil then
+ created_dir = ffi.string(created_dir)
+ end
+ return ret, failed_dir, created_dir
end
describe('os_mkdir', function()
@@ -774,33 +780,37 @@ describe('fs.c', function()
describe('os_mkdir_recurse', function()
itp('returns zero when given an already existing directory', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
- local ret, failed_str = os_mkdir_recurse('unit-test-directory', mode)
+ local ret, failed_dir, created_dir = os_mkdir_recurse('unit-test-directory', mode)
eq(0, ret)
- eq(nil, failed_str)
+ eq(nil, failed_dir)
+ eq(nil, created_dir)
end)
itp('fails to create a directory where there is a file', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
- local ret, failed_str = os_mkdir_recurse(
+ local ret, failed_dir, created_dir = os_mkdir_recurse(
'unit-test-directory/test.file', mode)
neq(0, ret)
- eq('unit-test-directory/test.file', failed_str)
+ eq('unit-test-directory/test.file', failed_dir)
+ eq(nil, created_dir)
end)
itp('fails to create a directory where there is a file in path', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
- local ret, failed_str = os_mkdir_recurse(
+ local ret, failed_dir, created_dir = os_mkdir_recurse(
'unit-test-directory/test.file/test', mode)
neq(0, ret)
- eq('unit-test-directory/test.file', failed_str)
+ eq('unit-test-directory/test.file', failed_dir)
+ eq(nil, created_dir)
end)
itp('succeeds to create a directory', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
- local ret, failed_str = os_mkdir_recurse(
+ local ret, failed_dir, created_dir = os_mkdir_recurse(
'unit-test-directory/new-dir-recurse', mode)
eq(0, ret)
- eq(nil, failed_str)
+ eq(nil, failed_dir)
+ ok(endswith(created_dir, 'unit-test-directory/new-dir-recurse'))
eq(true, os_isdir('unit-test-directory/new-dir-recurse'))
luv.fs_rmdir('unit-test-directory/new-dir-recurse')
eq(false, os_isdir('unit-test-directory/new-dir-recurse'))
@@ -808,10 +818,11 @@ describe('fs.c', function()
itp('succeeds to create a directory ending with ///', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
- local ret, failed_str = os_mkdir_recurse(
+ local ret, failed_dir, created_dir = os_mkdir_recurse(
'unit-test-directory/new-dir-recurse///', mode)
eq(0, ret)
- eq(nil, failed_str)
+ eq(nil, failed_dir)
+ ok(endswith(created_dir, 'unit-test-directory/new-dir-recurse'))
eq(true, os_isdir('unit-test-directory/new-dir-recurse'))
luv.fs_rmdir('unit-test-directory/new-dir-recurse')
eq(false, os_isdir('unit-test-directory/new-dir-recurse'))
@@ -819,10 +830,11 @@ describe('fs.c', function()
itp('succeeds to create a directory ending with /', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
- local ret, failed_str = os_mkdir_recurse(
+ local ret, failed_dir, created_dir = os_mkdir_recurse(
'unit-test-directory/new-dir-recurse/', mode)
eq(0, ret)
- eq(nil, failed_str)
+ eq(nil, failed_dir)
+ ok(endswith(created_dir, 'unit-test-directory/new-dir-recurse'))
eq(true, os_isdir('unit-test-directory/new-dir-recurse'))
luv.fs_rmdir('unit-test-directory/new-dir-recurse')
eq(false, os_isdir('unit-test-directory/new-dir-recurse'))
@@ -830,10 +842,11 @@ describe('fs.c', function()
itp('succeeds to create a directory tree', function()
local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR
- local ret, failed_str = os_mkdir_recurse(
+ local ret, failed_dir, created_dir = os_mkdir_recurse(
'unit-test-directory/new-dir-recurse/1/2/3', mode)
eq(0, ret)
- eq(nil, failed_str)
+ eq(nil, failed_dir)
+ ok(endswith(created_dir, 'unit-test-directory/new-dir-recurse'))
eq(true, os_isdir('unit-test-directory/new-dir-recurse'))
eq(true, os_isdir('unit-test-directory/new-dir-recurse/1'))
eq(true, os_isdir('unit-test-directory/new-dir-recurse/1/2'))