Merge pull request #23119 from zeertzjq/vim-9.0.0370

vim-patch:9.0.{0370,0379,0390,0397,0411,1446}: :defer and deferred delete
This commit is contained in:
zeertzjq
2023-04-16 15:59:32 +08:00
committed by GitHub
22 changed files with 653 additions and 184 deletions

View File

@@ -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.
When {object} is a |Blob| write the bytes to file {fname}
unmodified.
When {flags} contains "a" then 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.
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, also when binary mode is not specified.
{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")
<
'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.
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")

View File

@@ -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

View File

@@ -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);

View File

@@ -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.

View File

@@ -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,7 +478,43 @@ void emsg_funcname(const char *errmsg, const char *name)
}
}
/// Allocate a variable for the result of a function.
/// 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 = *arg;
int ret = OK;
// Get the arguments.
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) {
ret = FAIL;
break;
}
(*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()
@@ -480,34 +525,16 @@ void emsg_funcname(const char *errmsg, const char *name)
int get_func_tv(const char *name, int len, typval_T *rettv, char **arg, evalarg_T *const evalarg,
funcexe_T *funcexe)
{
char *argp;
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)) {
argp = skipwhite(argp + 1); // skip the '(' or ','
if (*argp == ')' || *argp == ',' || *argp == NUL) {
break;
}
if (eval1(&argp, &argvars[argcount], evalarg) == FAIL) {
ret = FAIL;
break;
}
argcount++;
if (*argp != ',') {
break;
}
}
if (*argp == ')') {
argp++;
} else {
ret = FAIL;
}
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".

View File

@@ -714,6 +714,18 @@ module.cmds = {
addr_type='ADDR_OTHER',
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),

View File

@@ -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.

View File

@@ -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);

View File

@@ -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"));

View File

@@ -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.

View File

@@ -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();

View File

@@ -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));

View File

@@ -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);
}

View File

@@ -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));

View File

@@ -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);

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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'))