# HG changeset patch # User Bram Moolenaar # Date 1647694803 -3600 # Node ID f34afadbef471a7246885fc68fbbaeab30befaec # Parent d14809743aa0a75fd2a493d181e0908adab3d6e9 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 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) diff --git a/runtime/doc/repeat.txt b/runtime/doc/repeat.txt --- 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 || 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 diff --git a/runtime/doc/todo.txt b/runtime/doc/todo.txt --- 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) diff --git a/src/alloc.c b/src/alloc.c --- 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) { diff --git a/src/digraph.c b/src/digraph.c --- 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; diff --git a/src/eval.c b/src/eval.c --- 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; diff --git a/src/ex_cmds.h b/src/ex_cmds.h --- 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), diff --git a/src/proto/scriptfile.pro b/src/proto/scriptfile.pro --- 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 : */ diff --git a/src/scriptfile.c b/src/scriptfile.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=" 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)); diff --git a/src/testdir/test_source.vim b/src/testdir/test_source.vim --- 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("") + 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("") + 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("") + let i = 1 + + \ 2 + + "\ comment + \ 3 + let g:Slnum2 = expand("") + 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("") + let g:Slnum1 = expand("") + let l =<< trim END + let g:Slnum2 = expand("") + let g:ScriptID2 = expand("") + END + new + call setline(1, l) + source + bw! + let g:ScriptID3 = expand("") + let g:Slnum3 = expand("") + 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 diff --git a/src/version.c b/src/version.c --- 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, diff --git a/src/vim9script.c b/src/vim9script.c --- 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;