From 0167649ce4071e60d985b65f3f9408ffb21cb58c Mon Sep 17 00:00:00 2001
From: zeertzjq <zeertzjq@outlook.com>
Date: Sun, 16 Apr 2023 11:07:48 +0800
Subject: [PATCH] vim-patch:9.0.0379: cleaning up after writefile() is a hassle

Problem:    Cleaning up after writefile() is a hassle.
Solution:   Add the 'D' flag to defer deleting the written file.  Very useful
            in tests.

https://github.com/vim/vim/commit/806a273f3c84ecd475913d901890bb1929be9a0a

Co-authored-by: Bram Moolenaar <Bram@vim.org>
---
 runtime/doc/builtin.txt             | 48 ++++++++++++++++++-----------
 src/nvim/eval/funcs.c               | 21 +++++++++++--
 src/nvim/eval/userfunc.c            | 24 +++++++++------
 test/old/testdir/test_quickfix.vim  | 22 -------------
 test/old/testdir/test_writefile.vim | 37 ++++++++++++++++------
 5 files changed, 91 insertions(+), 61 deletions(-)

diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index cb05718ab9..adb3164429 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -9507,31 +9507,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")
diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c
index 99e511a7a4..66fd663e5e 100644
--- a/src/nvim/eval/funcs.c
+++ b/src/nvim/eval/funcs.c
@@ -9296,6 +9296,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 +9310,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 +9331,12 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
   if (fname == NULL) {
     return;
   }
+
+  if (defer && get_current_funccal() == NULL) {
+    semsg(_(e_str_not_inside_function), "defer");
+    return;
+  }
+
   FileDescriptor fp;
   int error;
   if (*fname == NUL) {
@@ -9336,9 +9345,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 = xstrdup(fname),
+      };
+      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/userfunc.c b/src/nvim/eval/userfunc.c
index 9853622ee0..4b9bc1fdec 100644
--- a/src/nvim/eval/userfunc.c
+++ b/src/nvim/eval/userfunc.c
@@ -478,6 +478,7 @@ void emsg_funcname(const char *errmsg, const char *name)
 
 /// 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)
 {
@@ -3119,16 +3120,28 @@ static int ex_defer_inner(char *name, char **arg, evalarg_T *const evalarg)
 {
   typval_T argvars[MAX_FUNC_ARGS + 1];  // vars for arguments
   int argcount = 0;  // number of arguments found
-  int ret = FAIL;
 
   if (current_funccal == NULL) {
     semsg(_(e_str_not_inside_function), "defer");
     return FAIL;
   }
   if (get_func_arguments(arg, evalarg, false, argvars, &argcount) == FAIL) {
-    goto theend;
+    while (--argcount >= 0) {
+      tv_clear(&argvars[argcount]);
+    }
+    return FAIL;
   }
+  add_defer(name, argcount, argvars);
+  return OK;
+}
+
+/// 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);
@@ -3140,13 +3153,6 @@ static int ex_defer_inner(char *name, char **arg, evalarg_T *const evalarg)
     argcount--;
     dr->dr_argvars[argcount] = argvars[argcount];
   }
-  ret = OK;
-
-theend:
-  while (--argcount >= 0) {
-    tv_clear(&argvars[argcount]);
-  }
-  return ret;
 }
 
 /// Invoked after a function has finished: invoke ":defer" functions.
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_writefile.vim b/test/old/testdir/test_writefile.vim
index 6019cee193..5e6428ded8 100644
--- a/test/old/testdir/test_writefile.vim
+++ b/test/old/testdir/test_writefile.vim
@@ -924,19 +924,36 @@ 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
 
 " Check that buffer is written before triggering QuitPre