changeset 28139:f34afadbef47 v8.2.4594

patch 8.2.4594: need to write script to a file to be able to source them Commit: https://github.com/vim/vim/commit/36a5b6867bb6c0bd69c8da7d788000ab8a0b0ab0 Author: Yegappan Lakshmanan <yegappan@yahoo.com> Date: Sat Mar 19 12:56:51 2022 +0000 patch 8.2.4594: need to write script to a file to be able to source them Problem: Need to write script to a file to be able to source them. Solution: Make ":source" use lines from the current buffer. (Yegappan Lakshmanan et al., closes #9967)
author Bram Moolenaar <Bram@vim.org>
date Sat, 19 Mar 2022 14:00:03 +0100
parents d14809743aa0
children 4511c6ef2893
files runtime/doc/repeat.txt runtime/doc/todo.txt src/alloc.c src/digraph.c src/eval.c src/ex_cmds.h src/proto/scriptfile.pro src/scriptfile.c src/testdir/test_source.vim src/version.c src/vim9script.c
diffstat 11 files changed, 530 insertions(+), 19 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/repeat.txt
+++ b/runtime/doc/repeat.txt
@@ -197,6 +197,12 @@ For writing a Vim script, see chapter 41
 :so[urce] {file}	Read Ex commands from {file}.  These are commands that
 			start with a ":".
 			Triggers the |SourcePre| autocommand.
+
+:[range]so[urce] 	Read Ex commands from the [range] of lines in the
+			current buffer.  When sourcing commands from the
+			current buffer, the same script-ID |<SID>| is used
+			even if the buffer is sourced multiple times.
+
 							*:source!*
 :so[urce]! {file}	Read Vim commands from {file}.  These are commands
 			that are executed from Normal mode, like you type
--- a/runtime/doc/todo.txt
+++ b/runtime/doc/todo.txt
@@ -4333,12 +4333,6 @@ 7   Execute a function with standard opt
     restore option values.  Especially useful for new options.  Problem: how
     to avoid a performance penalty (esp. for string options)?
 -   range for ":exec", pass it on to the executed command.  (Webb)
-8   ":{range}source": source the lines from the current file.
-	You can already yank lines and use :@" to execute them.
-	Most of do_source() would not be used, need a new function.
-	It's easy when not doing breakpoints or profiling.
-    Requires copying the lines into a list and then creating a function to
-    execute lines from the list.  Similar to getnextac().
 7   ":include" command: just like ":source" but doesn't start a new scriptID?
     Will be tricky for the list of script names.
 8   Have a look at VSEL.  Would it be useful to include? (Bigham)
--- a/src/alloc.c
+++ b/src/alloc.c
@@ -845,7 +845,7 @@ ga_concat(garray_T *gap, char_u *s)
     void
 ga_concat_len(garray_T *gap, char_u *s, size_t len)
 {
-    if (s == NULL || *s == NUL)
+    if (s == NULL || *s == NUL || len == 0)
 	return;
     if (ga_grow(gap, (int)len) == OK)
     {
--- a/src/digraph.c
+++ b/src/digraph.c
@@ -2507,7 +2507,7 @@ ex_loadkeymap(exarg_T *eap)
     int		i;
     char_u	*save_cpo = p_cpo;
 
-    if (!getline_equal(eap->getline, eap->cookie, getsourceline))
+    if (!sourcing_a_script(eap))
     {
 	emsg(_(e_using_loadkeymap_not_in_sourced_file));
 	return;
--- a/src/eval.c
+++ b/src/eval.c
@@ -140,7 +140,7 @@ fill_evalarg_from_eap(evalarg_T *evalarg
     if (eap != NULL)
     {
 	evalarg->eval_cstack = eap->cstack;
-	if (getline_equal(eap->getline, eap->cookie, getsourceline))
+	if (sourcing_a_script(eap))
 	{
 	    evalarg->eval_getline = eap->getline;
 	    evalarg->eval_cookie = eap->cookie;
--- a/src/ex_cmds.h
+++ b/src/ex_cmds.h
@@ -1428,8 +1428,8 @@ EXCMD(CMD_snoremenu,	"snoremenu",	ex_men
 	EX_RANGE|EX_ZEROR|EX_EXTRA|EX_TRLBAR|EX_NOTRLCOM|EX_CTRLV|EX_CMDWIN|EX_LOCK_OK,
 	ADDR_OTHER),
 EXCMD(CMD_source,	"source",	ex_source,
-	EX_BANG|EX_FILE1|EX_TRLBAR|EX_SBOXOK|EX_CMDWIN|EX_LOCK_OK,
-	ADDR_NONE),
+	EX_RANGE|EX_DFLALL|EX_BANG|EX_FILE1|EX_TRLBAR|EX_SBOXOK|EX_CMDWIN|EX_LOCK_OK,
+	ADDR_LINES),
 EXCMD(CMD_sort,		"sort",		ex_sort,
 	EX_RANGE|EX_DFLALL|EX_WHOLEFOLD|EX_BANG|EX_EXTRA|EX_NOTRLCOM|EX_MODIFY,
 	ADDR_LINES),
--- a/src/proto/scriptfile.pro
+++ b/src/proto/scriptfile.pro
@@ -42,4 +42,5 @@ char_u *get_autoload_prefix(scriptitem_T
 char_u *may_prefix_autoload(char_u *name);
 char_u *autoload_name(char_u *name);
 int script_autoload(char_u *name, int reload);
+int sourcing_a_script(exarg_T *eap);
 /* vim: set ft=c : */
--- a/src/scriptfile.c
+++ b/src/scriptfile.c
@@ -18,6 +18,11 @@
 static garray_T		ga_loaded = {0, 0, sizeof(char_u *), 4, NULL};
 #endif
 
+// last used sequence number for sourcing scripts (current_sctx.sc_seq)
+#ifdef FEAT_EVAL
+static int		last_current_SID_seq = 0;
+#endif
+
 /*
  * Initialize the execution stack.
  */
@@ -1074,12 +1079,270 @@ ExpandPackAddDir(
     return OK;
 }
 
+/*
+ * Cookie used to source Ex commands from a buffer.
+ */
+typedef struct
+{
+    garray_T	lines_to_source;
+    int		lnum;
+    linenr_T	sourcing_lnum;
+} bufline_cookie_T;
+
+/*
+ * Concatenate a Vim script line if it starts with a line continuation into a
+ * growarray (excluding the continuation chars and leading whitespace).
+ * Growsize of the growarray may be changed to speed up concatenations!
+ *
+ * Returns TRUE if this line did begin with a continuation (the next line
+ * should also be considered, if it exists); FALSE otherwise.
+ */
+    static int
+concat_continued_line(
+    garray_T	*ga,
+    int		init_growsize,
+    char_u	*nextline,
+    int		options)
+{
+    int		comment_char = in_vim9script() ? '#' : '"';
+    char_u	*p = skipwhite(nextline);
+    int		contline;
+    int		do_vim9_all = in_vim9script()
+					&& options == GETLINE_CONCAT_ALL;
+    int		do_bar_cont = do_vim9_all
+					|| options == GETLINE_CONCAT_CONTBAR;
+
+    if (*p == NUL)
+	return FALSE;
+
+    // Concatenate the next line when it starts with a backslash.
+    /* Also check for a comment in between continuation lines: "\ */
+    // Also check for a Vim9 comment, empty line, line starting with '|',
+    // but not "||".
+    if ((p[0] == comment_char && p[1] == '\\' && p[2] == ' ')
+			|| (do_vim9_all && (*p == NUL
+				|| vim9_comment_start(p))))
+	return TRUE;
+
+    contline = (*p == '\\' || (do_bar_cont && p[0] == '|' && p[1] != '|'));
+    if (!contline)
+	return FALSE;
+
+    // Adjust the growsize to the current length to speed up concatenating many
+    // lines.
+    if (ga->ga_len > init_growsize)
+	ga->ga_growsize = ga->ga_len > 8000 ? 8000 : ga->ga_len;
+    if (*p == '\\')
+	ga_concat(ga, (char_u *)p + 1);
+    else if (*p == '|')
+    {
+	ga_concat(ga, (char_u *)" ");
+	ga_concat(ga, p);
+    }
+
+    return TRUE;
+}
+
+/*
+ * Get one full line from a sourced string (in-memory, no file).
+ * Called by do_cmdline() when it's called from source_using_linegetter().
+ *
+ * Returns a pointer to allocated line, or NULL for end-of-file.
+ */
+    static char_u *
+source_getbufline(
+    int			c UNUSED,
+    void		*cookie,
+    int			indent UNUSED,
+    getline_opt_T	opts)
+{
+    bufline_cookie_T	*p = cookie;
+    char_u		*line;
+    garray_T		ga;
+
+    SOURCING_LNUM = p->sourcing_lnum + 1;
+
+    if (p->lnum >= p->lines_to_source.ga_len)
+	return NULL;
+    line = ((char_u **)p->lines_to_source.ga_data)[p->lnum];
+
+    ga_init2(&ga, sizeof(char_u), 400);
+    ga_concat(&ga, (char_u *)line);
+    p->lnum++;
+
+    if ((opts != GETLINE_NONE) && vim_strchr(p_cpo, CPO_CONCAT) == NULL)
+    {
+	while (p->lnum < p->lines_to_source.ga_len)
+	{
+	    line = ((char_u **)p->lines_to_source.ga_data)[p->lnum];
+	    if (!concat_continued_line(&ga, 400, line, opts))
+		break;
+	    p->sourcing_lnum++;
+	    p->lnum++;
+	}
+    }
+    ga_append(&ga, NUL);
+    p->sourcing_lnum++;
+
+    return ga.ga_data;
+}
+
+/*
+ * Source Ex commands from the lines in 'cookie'.
+ */
+    static int
+do_sourcebuffer(
+    void	*cookie,
+    char_u	*scriptname)
+{
+    char_u		*save_sourcing_name = SOURCING_NAME;
+    linenr_T		save_sourcing_lnum = SOURCING_LNUM;
+    char_u		sourcing_name_buf[256];
+    sctx_T		save_current_sctx;
+#ifdef FEAT_EVAL
+    int			sid;
+    funccal_entry_T	funccalp_entry;
+    int			save_estack_compiling = estack_compiling;
+    scriptitem_T	*si = NULL;
+#endif
+    int			save_sticky_cmdmod_flags = sticky_cmdmod_flags;
+    int			retval = FAIL;
+    ESTACK_CHECK_DECLARATION
+
+    if (save_sourcing_name == NULL)
+	SOURCING_NAME = (char_u *)scriptname;
+    else
+    {
+	vim_snprintf((char *)sourcing_name_buf, sizeof(sourcing_name_buf),
+		"%s called at %s:%ld", scriptname, save_sourcing_name,
+		save_sourcing_lnum);
+	SOURCING_NAME = sourcing_name_buf;
+    }
+    SOURCING_LNUM = 0;
+
+    // Keep the sourcing name/lnum, for recursive calls.
+    estack_push(ETYPE_SCRIPT, scriptname, 0);
+    ESTACK_CHECK_SETUP
+
+    // "legacy" does not apply to commands in the script
+    sticky_cmdmod_flags = 0;
+
+    save_current_sctx = current_sctx;
+    current_sctx.sc_version = 1;  // default script version
+#ifdef FEAT_EVAL
+    estack_compiling = FALSE;
+    // Always use a new sequence number.
+    current_sctx.sc_seq = ++last_current_SID_seq;
+    current_sctx.sc_lnum = save_sourcing_lnum;
+    save_funccal(&funccalp_entry);
+
+    sid = find_script_by_name(scriptname);
+    if (sid < 0)
+    {
+	int		error = OK;
+
+	// First time sourcing this buffer, create a new script item.
+
+	sid = get_new_scriptitem(&error);
+	if (error == FAIL)
+	    goto theend;
+	current_sctx.sc_sid = sid;
+	si = SCRIPT_ITEM(current_sctx.sc_sid);
+	si->sn_name = vim_strsave(scriptname);
+	si->sn_state = SN_STATE_NEW;
+    }
+    else
+    {
+	// the buffer was sourced previously, reuse the script ID.
+	current_sctx.sc_sid = sid;
+	si = SCRIPT_ITEM(current_sctx.sc_sid);
+	si->sn_state = SN_STATE_RELOAD;
+    }
+#endif
+
+    retval = do_cmdline(NULL, source_getbufline, cookie,
+				DOCMD_VERBOSE | DOCMD_NOWAIT | DOCMD_REPEAT);
+
+    if (got_int)
+	emsg(_(e_interrupted));
+
+#ifdef FEAT_EVAL
+theend:
+#endif
+    ESTACK_CHECK_NOW
+    estack_pop();
+    current_sctx = save_current_sctx;
+    SOURCING_LNUM = save_sourcing_lnum;
+    SOURCING_NAME = save_sourcing_name;
+    sticky_cmdmod_flags = save_sticky_cmdmod_flags;
+#ifdef FEAT_EVAL
+    restore_funccal();
+    estack_compiling = save_estack_compiling;
+#endif
+
+    return retval;
+}
+
+/*
+ * :source Ex commands from the current buffer
+ */
+    static void
+cmd_source_buffer(exarg_T *eap)
+{
+    char_u		*line = NULL;
+    linenr_T		curr_lnum;
+    bufline_cookie_T	cp;
+    char_u		sname[32];
+
+    if (curbuf == NULL)
+	return;
+
+    // Use ":source buffer=<num>" as the script name
+    vim_snprintf((char *)sname, sizeof(sname), ":source buffer=%d",
+							curbuf->b_fnum);
+
+    ga_init2(&cp.lines_to_source, sizeof(char_u *), 100);
+
+    // Copy the lines from the buffer into a grow array
+    for (curr_lnum = eap->line1; curr_lnum <= eap->line2; curr_lnum++)
+    {
+	line = vim_strsave(ml_get(curr_lnum));
+	if (line == NULL)
+	    goto errret;
+	if (ga_add_string(&cp.lines_to_source, line) == FAIL)
+	    goto errret;
+	line = NULL;
+    }
+    cp.sourcing_lnum = 0;
+    cp.lnum = 0;
+
+    // Execute the Ex commands
+    do_sourcebuffer((void *)&cp, (char_u *)sname);
+
+errret:
+    vim_free(line);
+    ga_clear_strings(&cp.lines_to_source);
+}
+
     static void
 cmd_source(char_u *fname, exarg_T *eap)
 {
-    if (*fname == NUL)
-	emsg(_(e_argument_required));
+    if (*fname != NUL && eap != NULL && eap->addr_count > 0)
+    {
+	// if a filename is specified to :source, then a range is not allowed
+	emsg(_(e_no_range_allowed));
+	return;
+    }
 
+    if (eap != NULL && *fname == NUL)
+    {
+	if (eap->forceit)
+	    // a file name is needed to source normal mode commands
+	    emsg(_(e_argument_required));
+	else
+	    // source ex commands from the current buffer
+	    cmd_source_buffer(eap);
+    }
     else if (eap != NULL && eap->forceit)
 	// ":source!": read Normal mode commands
 	// Need to execute the commands directly.  This is required at least
@@ -1240,7 +1503,6 @@ do_source(
     int			    retval = FAIL;
     sctx_T		    save_current_sctx;
 #ifdef FEAT_EVAL
-    static int		    last_current_SID_seq = 0;
     funccal_entry_T	    funccalp_entry;
     int			    save_debug_break_level = debug_break_level;
     int			    sid;
@@ -2016,6 +2278,17 @@ getsourceline(
 }
 
 /*
+ * Returns TRUE if sourcing a script either from a file or a buffer.
+ * Otherwise returns FALSE.
+ */
+    int
+sourcing_a_script(exarg_T *eap)
+{
+    return (getline_equal(eap->getline, eap->cookie, getsourceline)
+	    || getline_equal(eap->getline, eap->cookie, source_getbufline));
+}
+
+/*
  * ":scriptencoding": Set encoding conversion for a sourced script.
  */
     void
@@ -2024,7 +2297,7 @@ ex_scriptencoding(exarg_T *eap)
     source_cookie_T	*sp;
     char_u		*name;
 
-    if (!getline_equal(eap->getline, eap->cookie, getsourceline))
+    if (!sourcing_a_script(eap))
     {
 	emsg(_(e_scriptencoding_used_outside_of_sourced_file));
 	return;
@@ -2055,7 +2328,7 @@ ex_scriptversion(exarg_T *eap UNUSED)
 {
     int		nr;
 
-    if (!getline_equal(eap->getline, eap->cookie, getsourceline))
+    if (!sourcing_a_script(eap))
     {
 	emsg(_(e_scriptversion_used_outside_of_sourced_file));
 	return;
@@ -2087,7 +2360,7 @@ ex_scriptversion(exarg_T *eap UNUSED)
     void
 ex_finish(exarg_T *eap)
 {
-    if (getline_equal(eap->getline, eap->cookie, getsourceline))
+    if (sourcing_a_script(eap))
 	do_finish(eap, FALSE);
     else
 	emsg(_(e_finish_used_outside_of_sourced_file));
--- a/src/testdir/test_source.vim
+++ b/src/testdir/test_source.vim
@@ -94,6 +94,12 @@ func Test_source_error()
   call assert_fails('scriptencoding utf-8', 'E167:')
   call assert_fails('finish', 'E168:')
   call assert_fails('scriptversion 2', 'E984:')
+  call assert_fails('source!', 'E471:')
+  new
+  call setline(1, ['', '', '', ''])
+  call assert_fails('1,3source Xscript.vim', 'E481:')
+  call assert_fails('1,3source! Xscript.vim', 'E481:')
+  bw!
 endfunc
 
 " Test for sourcing a script recursively
@@ -110,4 +116,233 @@ func Test_nested_script()
   call StopVimInTerminal(buf)
 endfunc
 
+" Test for sourcing a script from the current buffer
+func Test_source_buffer()
+  new
+  " Source a simple script
+  let lines =<< trim END
+    let a = "Test"
+    let b = 20
+
+    let c = [1.1]
+  END
+  call setline(1, lines)
+  source
+  call assert_equal(['Test', 20, [1.1]], [g:a, g:b, g:c])
+
+  " Source a range of lines in the current buffer
+  %d _
+  let lines =<< trim END
+    let a = 10
+    let a += 20
+    let a += 30
+    let a += 40
+  END
+  call setline(1, lines)
+  .source
+  call assert_equal(10, g:a)
+  3source
+  call assert_equal(40, g:a)
+  2,3source
+  call assert_equal(90, g:a)
+
+  " Source a script with line continuation lines
+  %d _
+  let lines =<< trim END
+    let m = [
+      \   1,
+      \   2,
+      \ ]
+    call add(m, 3)
+  END
+  call setline(1, lines)
+  source
+  call assert_equal([1, 2, 3], g:m)
+  " Source a script with line continuation lines and a comment
+  %d _
+  let lines =<< trim END
+    let m = [
+      "\ first entry
+      \   'a',
+      "\ second entry
+      \   'b',
+      \ ]
+    " third entry
+    call add(m, 'c')
+  END
+  call setline(1, lines)
+  source
+  call assert_equal(['a', 'b', 'c'], g:m)
+  " Source an incomplete line continuation line
+  %d _
+  let lines =<< trim END
+    let k = [
+      \
+  END
+  call setline(1, lines)
+  call assert_fails('source', 'E697:')
+  " Source a function with a for loop
+  %d _
+  let lines =<< trim END
+    let m = []
+    " test function
+    func! Xtest()
+      for i in range(5, 7)
+        call add(g:m, i)
+      endfor
+    endfunc
+    call Xtest()
+  END
+  call setline(1, lines)
+  source
+  call assert_equal([5, 6, 7], g:m)
+  " Source an empty buffer
+  %d _
+  source
+
+  " test for script local functions and variables
+  let lines =<< trim END
+    let s:var1 = 10
+    func s:F1()
+      let s:var1 += 1
+      return s:var1
+    endfunc
+    func s:F2()
+    endfunc
+    let g:ScriptID = expand("<SID>")
+  END
+  call setline(1, lines)
+  source
+  call assert_true(g:ScriptID != '')
+  call assert_true(exists('*' .. g:ScriptID .. 'F1'))
+  call assert_true(exists('*' .. g:ScriptID .. 'F2'))
+  call assert_equal(11, call(g:ScriptID .. 'F1', []))
+
+  " the same script ID should be used even if the buffer is sourced more than
+  " once
+  %d _
+  let lines =<< trim END
+    let g:ScriptID = expand("<SID>")
+    let g:Count += 1
+  END
+  call setline(1, lines)
+  let g:Count = 0
+  source
+  call assert_true(g:ScriptID != '')
+  let scid = g:ScriptID
+  source
+  call assert_equal(scid, g:ScriptID)
+  call assert_equal(2, g:Count)
+  source
+  call assert_equal(scid, g:ScriptID)
+  call assert_equal(3, g:Count)
+
+  " test for the script line number
+  %d _
+  let lines =<< trim END
+    " comment
+    let g:Slnum1 = expand("<slnum>")
+    let i = 1 +
+           \ 2 +
+          "\ comment
+           \ 3
+    let g:Slnum2 = expand("<slnum>")
+  END
+  call setline(1, lines)
+  source
+  call assert_equal('2', g:Slnum1)
+  call assert_equal('7', g:Slnum2)
+
+  " test for retaining the same script number across source calls
+  let lines =<< trim END
+     let g:ScriptID1 = expand("<SID>")
+     let g:Slnum1 = expand("<slnum>")
+     let l =<< trim END
+       let g:Slnum2 = expand("<slnum>")
+       let g:ScriptID2 = expand("<SID>")
+     END
+     new
+     call setline(1, l)
+     source
+     bw!
+     let g:ScriptID3 = expand("<SID>")
+     let g:Slnum3 = expand("<slnum>")
+  END
+  call writefile(lines, 'Xscript')
+  source Xscript
+  call assert_true(g:ScriptID1 != g:ScriptID2)
+  call assert_equal(g:ScriptID1, g:ScriptID3)
+  call assert_equal('2', g:Slnum1)
+  call assert_equal('1', g:Slnum2)
+  call assert_equal('12', g:Slnum3)
+  call delete('Xscript')
+
+  " test for sourcing a heredoc
+  %d _
+  let lines =<< trim END
+     let a = 1
+     let heredoc =<< trim DATA
+        red
+          green
+        blue
+     DATA
+     let b = 2
+  END
+  call setline(1, lines)
+  source
+  call assert_equal(['red', '  green', 'blue'], g:heredoc)
+
+  " test for a while and for statement
+  %d _
+  let lines =<< trim END
+     let a = 0
+     let b = 1
+     while b <= 10
+       let a += 10
+       let b += 1
+     endwhile
+     for i in range(5)
+       let a += 10
+     endfor
+  END
+  call setline(1, lines)
+  source
+  call assert_equal(150, g:a)
+
+  " test for sourcing the same buffer multiple times after changing a function
+  %d _
+  let lines =<< trim END
+     func Xtestfunc()
+       return "one"
+     endfunc
+  END
+  call setline(1, lines)
+  source
+  call assert_equal("one", Xtestfunc())
+  call setline(2, '  return "two"')
+  source
+  call assert_equal("two", Xtestfunc())
+  call setline(2, '  return "three"')
+  source
+  call assert_equal("three", Xtestfunc())
+  delfunc Xtestfunc
+
+  " test for sourcing a Vim9 script
+  %d _
+  let lines =<< trim END
+     vim9script
+
+     # check dict
+     var x: number = 10
+     def g:Xtestfunc(): number
+       return x
+     enddef
+  END
+  call setline(1, lines)
+  source
+  call assert_equal(10, Xtestfunc())
+
+  %bw!
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
--- a/src/version.c
+++ b/src/version.c
@@ -751,6 +751,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    4594,
+/**/
     4593,
 /**/
     4592,
--- a/src/vim9script.c
+++ b/src/vim9script.c
@@ -71,7 +71,7 @@ ex_vim9script(exarg_T *eap UNUSED)
     int		    found_noclear = FALSE;
     char_u	    *p;
 
-    if (!getline_equal(eap->getline, eap->cookie, getsourceline))
+    if (!sourcing_a_script(eap))
     {
 	emsg(_(e_vim9script_can_only_be_used_in_script));
 	return;
@@ -633,7 +633,7 @@ ex_import(exarg_T *eap)
     char_u	*cmd_end;
     evalarg_T	evalarg;
 
-    if (!getline_equal(eap->getline, eap->cookie, getsourceline))
+    if (!sourcing_a_script(eap))
     {
 	emsg(_(e_import_can_only_be_used_in_script));
 	return;