aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/eval.txt57
-rw-r--r--runtime/doc/options.txt4
-rw-r--r--runtime/syntax/modula3.vim4
-rw-r--r--src/nvim/eval.c160
-rw-r--r--src/nvim/eval/userfunc.c17
-rw-r--r--src/nvim/eval/vars.c164
-rw-r--r--src/nvim/globals.h5
-rw-r--r--test/old/testdir/test_debugger.vim33
-rw-r--r--test/old/testdir/test_eval_stuff.vim5
-rw-r--r--test/old/testdir/test_expr.vim56
-rw-r--r--test/old/testdir/test_let.vim120
-rw-r--r--test/old/testdir/test_scriptnames.vim4
12 files changed, 574 insertions, 55 deletions
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt
index 351690f4df..1ff6e3c360 100644
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -1407,6 +1407,34 @@ to be doubled. These two commands are equivalent: >
------------------------------------------------------------------------------
+interpolated-string *$quote* *interpolated-string*
+
+$"string" interpolated string constant *expr-$quote*
+$'string' interpolated literal string constant *expr-$'*
+
+Interpolated strings are an extension of the |string| and |literal-string|,
+allowing the inclusion of Vim script expressions (see |expr1|). Any
+expression returning a value can be enclosed between curly braces. The value
+is converted to a string. All the text and results of the expressions
+are concatenated to make a new string.
+ *E1278*
+To include an opening brace '{' or closing brace '}' in the string content
+double it. For double quoted strings using a backslash also works. A single
+closing brace '}' will result in an error.
+
+Examples: >
+ let your_name = input("What's your name? ")
+< What's your name? Peter ~
+>
+ echo
+ echo $"Hello, {your_name}!"
+< Hello, Peter! ~
+>
+ echo $"The square root of {{9}} is {sqrt(9)}"
+< The square root of {9} is 3.0 ~
+
+
+------------------------------------------------------------------------------
option *expr-option* *E112* *E113*
&option option value, local value if possible
@@ -2540,14 +2568,30 @@ This does NOT work: >
*:let=<<* *:let-heredoc*
*E990* *E991* *E172* *E221* *E1145*
-:let {var-name} =<< [trim] {endmarker}
+:let {var-name} =<< [trim] [eval] {endmarker}
text...
text...
{endmarker}
Set internal variable {var-name} to a |List|
containing the lines of text bounded by the string
- {endmarker}. The lines of text is used as a
- |literal-string|.
+ {endmarker}.
+
+ If "eval" is not specified, then each line of text is
+ used as a |literal-string|, except that single quotes
+ does not need to be doubled.
+ If "eval" is specified, then any Vim expression in the
+ form {expr} is evaluated and the result replaces the
+ expression, like with |interpolated-string|.
+ Example where $HOME is expanded: >
+ let lines =<< trim eval END
+ some text
+ See the file {$HOME}/.vimrc
+ more text
+ END
+< There can be multiple Vim expressions in a single line
+ but an expression cannot span multiple lines. If any
+ expression evaluation fails, then the assignment fails.
+
{endmarker} must not contain white space.
{endmarker} cannot start with a lower case character.
The last line should end only with the {endmarker}
@@ -2597,6 +2641,13 @@ text...
1 2 3 4
5 6 7 8
DATA
+
+ let code =<< trim eval CODE
+ let v = {10 + 20}
+ let h = "{$HOME}"
+ let s = "{Str1()} abc {Str2()}"
+ let n = {MyFunc(3, 4)}
+ CODE
<
*E121*
:let {var-name} .. List the value of variable {var-name}. Multiple
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index b4cad51990..dcaf37af20 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -3332,8 +3332,8 @@ A jump table for the options with a short description can be found at |Q_op|.
If the expression starts with s: or |<SID>|, then it is replaced with
the script ID (|local-function|). Example: >
- set includeexpr=s:MyIncludeExpr(v:fname)
- set includeexpr=<SID>SomeIncludeExpr(v:fname)
+ setlocal includeexpr=s:MyIncludeExpr(v:fname)
+ setlocal includeexpr=<SID>SomeIncludeExpr(v:fname)
<
The expression will be evaluated in the |sandbox| when set from a
modeline, see |sandbox-option|.
diff --git a/runtime/syntax/modula3.vim b/runtime/syntax/modula3.vim
index 390a1a90ff..67243db600 100644
--- a/runtime/syntax/modula3.vim
+++ b/runtime/syntax/modula3.vim
@@ -84,9 +84,7 @@ syn case ignore
let s:digits = "0123456789ABCDEF"
for s:radix in range(2, 16)
- " Nvim does not support interpolated strings yet.
- " exe $'syn match modula3Integer "\<{s:radix}_[{s:digits[:s:radix - 1]}]\+L\=\>"'
- exe 'syn match modula3Integer "\<' .. s:radix .. '_[' .. s:digits[:s:radix - 1] .. ']\+L\=\>"'
+ exe $'syn match modula3Integer "\<{s:radix}_[{s:digits[:s:radix - 1]}]\+L\=\>"'
endfor
unlet s:digits s:radix
diff --git a/src/nvim/eval.c b/src/nvim/eval.c
index b240c36977..a30f9146c5 100644
--- a/src/nvim/eval.c
+++ b/src/nvim/eval.c
@@ -3063,12 +3063,12 @@ static int eval7(char **arg, typval_T *rettv, evalarg_T *const evalarg, bool wan
// String constant: "string".
case '"':
- ret = eval_string(arg, rettv, evaluate);
+ ret = eval_string(arg, rettv, evaluate, false);
break;
// Literal string constant: 'str''ing'.
case '\'':
- ret = eval_lit_string(arg, rettv, evaluate);
+ ret = eval_lit_string(arg, rettv, evaluate, false);
break;
// List: [expr, expr]
@@ -3100,8 +3100,13 @@ static int eval7(char **arg, typval_T *rettv, evalarg_T *const evalarg, bool wan
ret = eval_option((const char **)arg, rettv, evaluate);
break;
// Environment variable: $VAR.
+ // Interpolated string: $"string" or $'string'.
case '$':
- ret = eval_env_var(arg, rettv, evaluate);
+ if ((*arg)[1] == '"' || (*arg)[1] == '\'') {
+ ret = eval_interp_string(arg, rettv, evaluate);
+ } else {
+ ret = eval_env_var(arg, rettv, evaluate);
+ }
break;
// Register contents: @r.
@@ -3863,35 +3868,63 @@ static int eval_number(char **arg, typval_T *rettv, bool evaluate, bool want_str
return OK;
}
-/// Allocate a variable for a string constant.
+/// Evaluate a string constant and put the result in "rettv".
+/// "*arg" points to the double quote or to after it when "interpolate" is true.
+/// When "interpolate" is true reduce "{{" to "{", reduce "}}" to "}" and stop
+/// at a single "{".
///
/// @return OK or FAIL.
-static int eval_string(char **arg, typval_T *rettv, int evaluate)
+static int eval_string(char **arg, typval_T *rettv, bool evaluate, bool interpolate)
{
char *p;
- unsigned int extra = 0;
+ const char *const arg_end = *arg + strlen(*arg);
+ unsigned int extra = interpolate ? 1 : 0;
+ const int off = interpolate ? 0 : 1;
// Find the end of the string, skipping backslashed characters.
- for (p = *arg + 1; *p != NUL && *p != '"'; MB_PTR_ADV(p)) {
+ for (p = *arg + off; *p != NUL && *p != '"'; MB_PTR_ADV(p)) {
if (*p == '\\' && p[1] != NUL) {
p++;
// A "\<x>" form occupies at least 4 characters, and produces up
// to 9 characters (6 for the char and 3 for a modifier):
// reserve space for 5 extra.
if (*p == '<') {
+ int modifiers = 0;
+ int flags = FSK_KEYCODE | FSK_IN_STRING;
+
extra += 5;
+
+ // Skip to the '>' to avoid using '{' inside for string
+ // interpolation.
+ if (p[1] != '*') {
+ flags |= FSK_SIMPLIFY;
+ }
+ if (find_special_key((const char **)&p, (size_t)(arg_end - p),
+ &modifiers, flags, NULL) != 0) {
+ p--; // leave "p" on the ">"
+ }
}
+ } else if (interpolate && (*p == '{' || *p == '}')) {
+ if (*p == '{' && p[1] != '{') { // start of expression
+ break;
+ }
+ p++;
+ if (p[-1] == '}' && *p != '}') { // single '}' is an error
+ semsg(_(e_stray_closing_curly_str), *arg);
+ return FAIL;
+ }
+ extra--; // "{{" becomes "{", "}}" becomes "}"
}
}
- if (*p != '"') {
+ if (*p != '"' && !(interpolate && *p == '{')) {
semsg(_("E114: Missing quote: %s"), *arg);
return FAIL;
}
// If only parsing, set *arg and return here
if (!evaluate) {
- *arg = p + 1;
+ *arg = p + off;
return OK;
}
@@ -3902,7 +3935,7 @@ static int eval_string(char **arg, typval_T *rettv, int evaluate)
rettv->vval.v_string = xmalloc((size_t)len);
char *end = rettv->vval.v_string;
- for (p = *arg + 1; *p != NUL && *p != '"';) {
+ for (p = *arg + off; *p != NUL && *p != '"';) {
if (*p == '\\') {
switch (*++p) {
case 'b':
@@ -3975,7 +4008,8 @@ static int eval_string(char **arg, typval_T *rettv, int evaluate)
if (p[1] != '*') {
flags |= FSK_SIMPLIFY;
}
- extra = trans_special((const char **)&p, strlen(p), end, flags, false, NULL);
+ extra = trans_special((const char **)&p, (size_t)(arg_end - p),
+ end, flags, false, NULL);
if (extra != 0) {
end += extra;
if (end >= rettv->vval.v_string + len) {
@@ -3991,11 +4025,17 @@ static int eval_string(char **arg, typval_T *rettv, int evaluate)
break;
}
} else {
+ if (interpolate && (*p == '{' || *p == '}')) {
+ if (*p == '{' && p[1] != '{') { // start of expression
+ break;
+ }
+ p++; // reduce "{{" to "{" and "}}" to "}"
+ }
mb_copy_char((const char **)&p, &end);
}
}
*end = NUL;
- if (*p != NUL) { // just in case
+ if (*p == '"' && !interpolate) {
p++;
}
*arg = p;
@@ -4004,52 +4044,130 @@ static int eval_string(char **arg, typval_T *rettv, int evaluate)
}
/// Allocate a variable for a 'str''ing' constant.
+/// When "interpolate" is true reduce "{{" to "{" and stop at a single "{".
///
-/// @return OK or FAIL.
-static int eval_lit_string(char **arg, typval_T *rettv, int evaluate)
+/// @return OK when a "rettv" was set to the string.
+/// FAIL on error, "rettv" is not set.
+static int eval_lit_string(char **arg, typval_T *rettv, bool evaluate, bool interpolate)
{
char *p;
- int reduce = 0;
+ int reduce = interpolate ? -1 : 0;
+ const int off = interpolate ? 0 : 1;
// Find the end of the string, skipping ''.
- for (p = *arg + 1; *p != NUL; MB_PTR_ADV(p)) {
+ for (p = *arg + off; *p != NUL; MB_PTR_ADV(p)) {
if (*p == '\'') {
if (p[1] != '\'') {
break;
}
reduce++;
p++;
+ } else if (interpolate) {
+ if (*p == '{') {
+ if (p[1] != '{') {
+ break;
+ }
+ p++;
+ reduce++;
+ } else if (*p == '}') {
+ p++;
+ if (*p != '}') {
+ semsg(_(e_stray_closing_curly_str), *arg);
+ return FAIL;
+ }
+ reduce++;
+ }
}
}
- if (*p != '\'') {
+ if (*p != '\'' && !(interpolate && *p == '{')) {
semsg(_("E115: Missing quote: %s"), *arg);
return FAIL;
}
// If only parsing return after setting "*arg"
if (!evaluate) {
- *arg = p + 1;
+ *arg = p + off;
return OK;
}
- // Copy the string into allocated memory, handling '' to ' reduction.
+ // Copy the string into allocated memory, handling '' to ' reduction and
+ // any expressions.
char *str = xmalloc((size_t)((p - *arg) - reduce));
rettv->v_type = VAR_STRING;
rettv->vval.v_string = str;
- for (p = *arg + 1; *p != NUL;) {
+ for (p = *arg + off; *p != NUL;) {
if (*p == '\'') {
if (p[1] != '\'') {
break;
}
p++;
+ } else if (interpolate && (*p == '{' || *p == '}')) {
+ if (*p == '{' && p[1] != '{') {
+ break;
+ }
+ p++;
}
mb_copy_char((const char **)&p, &str);
}
*str = NUL;
- *arg = p + 1;
+ *arg = p + off;
+
+ return OK;
+}
+/// Evaluate a single or double quoted string possibly containing expressions.
+/// "arg" points to the '$'. The result is put in "rettv".
+///
+/// @return OK or FAIL.
+int eval_interp_string(char **arg, typval_T *rettv, bool evaluate)
+{
+ int ret = OK;
+
+ garray_T ga;
+ ga_init(&ga, 1, 80);
+
+ // *arg is on the '$' character, move it to the first string character.
+ (*arg)++;
+ const int quote = (uint8_t)(**arg);
+ (*arg)++;
+
+ for (;;) {
+ typval_T tv;
+ // Get the string up to the matching quote or to a single '{'.
+ // "arg" is advanced to either the quote or the '{'.
+ if (quote == '"') {
+ ret = eval_string(arg, &tv, evaluate, true);
+ } else {
+ ret = eval_lit_string(arg, &tv, evaluate, true);
+ }
+ if (ret == FAIL) {
+ break;
+ }
+ if (evaluate) {
+ ga_concat(&ga, tv.vval.v_string);
+ tv_clear(&tv);
+ }
+
+ if (**arg != '{') {
+ // found terminating quote
+ (*arg)++;
+ break;
+ }
+ char *p = eval_one_expr_in_str(*arg, &ga, evaluate);
+ if (p == NULL) {
+ ret = FAIL;
+ break;
+ }
+ *arg = p;
+ }
+
+ rettv->v_type = VAR_STRING;
+ if (ret != FAIL && evaluate) {
+ ga_append(&ga, NUL);
+ }
+ rettv->vval.v_string = ga.ga_data;
return OK;
}
diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c
index 67c73924c8..67b1e53a35 100644
--- a/src/nvim/eval/userfunc.c
+++ b/src/nvim/eval/userfunc.c
@@ -2483,10 +2483,19 @@ void ex_function(exarg_T *eap)
&& (!ASCII_ISALNUM(p[2])
|| (p[2] == 't' && !ASCII_ISALNUM(p[3]))))) {
p = skipwhite(arg + 3);
- if (strncmp(p, "trim", 4) == 0) {
- // Ignore leading white space.
- p = skipwhite(p + 4);
- heredoc_trimmed = xstrnsave(theline, (size_t)(skipwhite(theline) - theline));
+ while (true) {
+ if (strncmp(p, "trim", 4) == 0) {
+ // Ignore leading white space.
+ p = skipwhite(p + 4);
+ heredoc_trimmed = xstrnsave(theline, (size_t)(skipwhite(theline) - theline));
+ continue;
+ }
+ if (strncmp(p, "eval", 4) == 0) {
+ // Ignore leading white space.
+ p = skipwhite(p + 4);
+ continue;
+ }
+ break;
}
skip_until = xstrnsave(p, (size_t)(skiptowhite(p) - p));
do_concat = false;
diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c
index b8a8f39437..d3ca2624eb 100644
--- a/src/nvim/eval/vars.c
+++ b/src/nvim/eval/vars.c
@@ -53,6 +53,96 @@
static const char *e_letunexp = N_("E18: Unexpected characters in :let");
static const char *e_lock_unlock = N_("E940: Cannot lock or unlock variable %s");
+/// Evaluate one Vim expression {expr} in string "p" and append the
+/// resulting string to "gap". "p" points to the opening "{".
+/// When "evaluate" is false only skip over the expression.
+/// Return a pointer to the character after "}", NULL for an error.
+char *eval_one_expr_in_str(char *p, garray_T *gap, bool evaluate)
+{
+ char *block_start = skipwhite(p + 1); // skip the opening {
+ char *block_end = block_start;
+
+ if (*block_start == NUL) {
+ semsg(_(e_missing_close_curly_str), p);
+ return NULL;
+ }
+ if (skip_expr(&block_end, NULL) == FAIL) {
+ return NULL;
+ }
+ block_end = skipwhite(block_end);
+ if (*block_end != '}') {
+ semsg(_(e_missing_close_curly_str), p);
+ return NULL;
+ }
+ if (evaluate) {
+ *block_end = NUL;
+ char *expr_val = eval_to_string(block_start, true);
+ *block_end = '}';
+ if (expr_val == NULL) {
+ return NULL;
+ }
+ ga_concat(gap, expr_val);
+ xfree(expr_val);
+ }
+
+ return block_end + 1;
+}
+
+/// Evaluate all the Vim expressions {expr} in "str" and return the resulting
+/// string in allocated memory. "{{" is reduced to "{" and "}}" to "}".
+/// Used for a heredoc assignment.
+/// Returns NULL for an error.
+char *eval_all_expr_in_str(char *str)
+{
+ garray_T ga;
+ ga_init(&ga, 1, 80);
+ char *p = str;
+
+ while (*p != NUL) {
+ bool escaped_brace = false;
+
+ // Look for a block start.
+ char *lit_start = p;
+ while (*p != '{' && *p != '}' && *p != NUL) {
+ p++;
+ }
+
+ if (*p != NUL && *p == p[1]) {
+ // Escaped brace, unescape and continue.
+ // Include the brace in the literal string.
+ p++;
+ escaped_brace = true;
+ } else if (*p == '}') {
+ semsg(_(e_stray_closing_curly_str), str);
+ ga_clear(&ga);
+ return NULL;
+ }
+
+ // Append the literal part.
+ ga_concat_len(&ga, lit_start, (size_t)(p - lit_start));
+
+ if (*p == NUL) {
+ break;
+ }
+
+ if (escaped_brace) {
+ // Skip the second brace.
+ p++;
+ continue;
+ }
+
+ // Evaluate the expression and append the result.
+ p = eval_one_expr_in_str(p, &ga, true);
+ if (p == NULL) {
+ ga_clear(&ga);
+ return NULL;
+ }
+ }
+ ga_append(&ga, NUL);
+
+ return ga.ga_data;
+}
+
/// Get a list of lines from a HERE document. The here document is a list of
/// lines surrounded by a marker.
/// cmd << {marker}
@@ -65,7 +155,7 @@ static const char *e_lock_unlock = N_("E940: Cannot lock or unlock variable %s")
/// marker, then the leading indentation before the lines (matching the
/// indentation in the 'cmd' line) is stripped.
///
-/// @return a List with {lines} or NULL.
+/// @return a List with {lines} or NULL on failure.
static list_T *heredoc_get(exarg_T *eap, char *cmd)
{
char *marker;
@@ -81,20 +171,33 @@ static list_T *heredoc_get(exarg_T *eap, char *cmd)
// Check for the optional 'trim' word before the marker
cmd = skipwhite(cmd);
- if (strncmp(cmd, "trim", 4) == 0
- && (cmd[4] == NUL || ascii_iswhite(cmd[4]))) {
- cmd = skipwhite(cmd + 4);
-
- // Trim the indentation from all the lines in the here document.
- // The amount of indentation trimmed is the same as the indentation of
- // the first line after the :let command line. To find the end marker
- // the indent of the :let command line is trimmed.
- p = *eap->cmdlinep;
- while (ascii_iswhite(*p)) {
- p++;
- marker_indent_len++;
+ bool evalstr = false;
+ bool eval_failed = false;
+ while (true) {
+ if (strncmp(cmd, "trim", 4) == 0
+ && (cmd[4] == NUL || ascii_iswhite(cmd[4]))) {
+ cmd = skipwhite(cmd + 4);
+
+ // Trim the indentation from all the lines in the here document.
+ // The amount of indentation trimmed is the same as the indentation
+ // of the first line after the :let command line. To find the end
+ // marker the indent of the :let command line is trimmed.
+ p = *eap->cmdlinep;
+ while (ascii_iswhite(*p)) {
+ p++;
+ marker_indent_len++;
+ }
+ text_indent_len = -1;
+
+ continue;
}
- text_indent_len = -1;
+ if (strncmp(cmd, "eval", 4) == 0
+ && (cmd[4] == NUL || ascii_iswhite(cmd[4]))) {
+ cmd = skipwhite(cmd + 4);
+ evalstr = true;
+ continue;
+ }
+ break;
}
// The marker is the next word.
@@ -115,12 +218,14 @@ static list_T *heredoc_get(exarg_T *eap, char *cmd)
return NULL;
}
+ char *theline = NULL;
list_T *l = tv_list_alloc(0);
for (;;) {
int mi = 0;
int ti = 0;
- char *theline = eap->getline(NUL, eap->cookie, 0, false);
+ xfree(theline);
+ theline = eap->getline(NUL, eap->cookie, 0, false);
if (theline == NULL) {
semsg(_("E990: Missing end marker '%s'"), marker);
break;
@@ -133,9 +238,15 @@ static list_T *heredoc_get(exarg_T *eap, char *cmd)
mi = marker_indent_len;
}
if (strcmp(marker, theline + mi) == 0) {
- xfree(theline);
break;
}
+
+ // If expression evaluation failed in the heredoc, then skip till the
+ // end marker.
+ if (eval_failed) {
+ continue;
+ }
+
if (text_indent_len == -1 && *theline != NUL) {
// set the text indent from the first line.
p = theline;
@@ -155,11 +266,28 @@ static list_T *heredoc_get(exarg_T *eap, char *cmd)
}
}
- tv_list_append_string(l, theline + ti, -1);
- xfree(theline);
+ char *str = theline + ti;
+ if (evalstr && !eap->skip) {
+ str = eval_all_expr_in_str(str);
+ if (str == NULL) {
+ // expression evaluation failed
+ eval_failed = true;
+ continue;
+ }
+ xfree(theline);
+ theline = str;
+ }
+
+ tv_list_append_string(l, str, -1);
}
+ xfree(theline);
xfree(text_indent);
+ if (eval_failed) {
+ // expression evaluation in the heredoc failed
+ tv_list_free(l);
+ return NULL;
+ }
return l;
}
diff --git a/src/nvim/globals.h b/src/nvim/globals.h
index e406d93494..11888a5df8 100644
--- a/src/nvim/globals.h
+++ b/src/nvim/globals.h
@@ -1024,6 +1024,11 @@ EXTERN const char e_highlight_group_name_too_long[] INIT(= N_("E1249: Highlight
EXTERN const char e_invalid_line_number_nr[] INIT(= N_("E966: Invalid line number: %ld"));
+EXTERN char e_stray_closing_curly_str[]
+INIT(= N_("E1278: Stray '}' without a matching '{': %s"));
+EXTERN char e_missing_close_curly_str[]
+INIT(= N_("E1279: Missing '}': %s"));
+
EXTERN const char e_undobang_cannot_redo_or_move_branch[]
INIT(= N_("E5767: Cannot use :undo! to redo or move to a different undo branch"));
diff --git a/test/old/testdir/test_debugger.vim b/test/old/testdir/test_debugger.vim
index f5177c8fb2..18616e8717 100644
--- a/test/old/testdir/test_debugger.vim
+++ b/test/old/testdir/test_debugger.vim
@@ -348,6 +348,39 @@ func Test_Debugger_breakadd()
call assert_fails('breakadd file Xtest.vim /\)/', 'E55:')
endfunc
+" Test for expression breakpoint set using ":breakadd expr <expr>"
+func Test_Debugger_breakadd_expr()
+ CheckRunVimInTerminal
+ let lines =<< trim END
+ let g:Xtest_var += 1
+ END
+ call writefile(lines, 'Xtest.vim')
+
+ " Start Vim in a terminal
+ let buf = RunVimInTerminal('Xtest.vim', {})
+ call RunDbgCmd(buf, ':let g:Xtest_var = 10')
+ call RunDbgCmd(buf, ':breakadd expr g:Xtest_var')
+ call RunDbgCmd(buf, ':source %')
+ let expected =<< eval trim END
+ Oldval = "10"
+ Newval = "11"
+ {fnamemodify('Xtest.vim', ':p')}
+ line 1: let g:Xtest_var += 1
+ END
+ call RunDbgCmd(buf, ':source %', expected)
+ call RunDbgCmd(buf, 'cont')
+ let expected =<< eval trim END
+ Oldval = "11"
+ Newval = "12"
+ {fnamemodify('Xtest.vim', ':p')}
+ line 1: let g:Xtest_var += 1
+ END
+ call RunDbgCmd(buf, ':source %', expected)
+
+ call StopVimInTerminal(buf)
+ call delete('Xtest.vim')
+endfunc
+
func Test_Backtrace_Through_Source()
CheckRunVimInTerminal
CheckCWD
diff --git a/test/old/testdir/test_eval_stuff.vim b/test/old/testdir/test_eval_stuff.vim
index 90e3942c4d..7acc91c17b 100644
--- a/test/old/testdir/test_eval_stuff.vim
+++ b/test/old/testdir/test_eval_stuff.vim
@@ -407,4 +407,9 @@ func Test_modified_char_no_escape_special()
nunmap <M-…>
endfunc
+func Test_eval_string_in_special_key()
+ " this was using the '{' inside <> as the start of an interpolated string
+ silent! echo 0{1-$"\<S--{>n|nö%
+endfunc
+
" vim: shiftwidth=2 sts=2 expandtab
diff --git a/test/old/testdir/test_expr.vim b/test/old/testdir/test_expr.vim
index c8b7fde100..dee7266bb5 100644
--- a/test/old/testdir/test_expr.vim
+++ b/test/old/testdir/test_expr.vim
@@ -848,4 +848,60 @@ func Test_float_compare()
call CheckLegacyAndVim9Success(lines)
endfunc
+func Test_string_interp()
+ let lines =<< trim END
+ call assert_equal('', $"")
+ call assert_equal('foobar', $"foobar")
+ #" Escaping rules.
+ call assert_equal('"foo"{bar}', $"\"foo\"{{bar}}")
+ call assert_equal('"foo"{bar}', $'"foo"{{bar}}')
+ call assert_equal('foobar', $"{"foo"}" .. $'{'bar'}')
+ #" Whitespace before/after the expression.
+ call assert_equal('3', $"{ 1 + 2 }")
+ #" String conversion.
+ call assert_equal('hello from ' .. v:version, $"hello from {v:version}")
+ call assert_equal('hello from ' .. v:version, $'hello from {v:version}')
+ #" Paper over a small difference between VimScript behaviour.
+ call assert_equal(string(v:true), $"{v:true}")
+ call assert_equal('(1+1=2)', $"(1+1={1 + 1})")
+ #" Hex-escaped opening brace: char2nr('{') == 0x7b
+ call assert_equal('esc123ape', $"esc{123}ape")
+ call assert_equal('me{}me', $"me{"\x7b"}\x7dme")
+ VAR var1 = "sun"
+ VAR var2 = "shine"
+ call assert_equal('sunshine', $"{var1}{var2}")
+ call assert_equal('sunsunsun', $"{var1->repeat(3)}")
+ #" Multibyte strings.
+ call assert_equal('say ハロー・ワールド', $"say {'ハロー・ワールド'}")
+ #" Nested.
+ call assert_equal('foobarbaz', $"foo{$"{'bar'}"}baz")
+ #" Do not evaluate blocks when the expr is skipped.
+ VAR tmp = 0
+ if v:false
+ echo "${ LET tmp += 1 }"
+ endif
+ call assert_equal(0, tmp)
+
+ #" Stray closing brace.
+ call assert_fails('echo $"moo}"', 'E1278:')
+ #" Undefined variable in expansion.
+ call assert_fails('echo $"{moo}"', 'E121:')
+ #" Empty blocks are rejected.
+ call assert_fails('echo $"{}"', 'E15:')
+ call assert_fails('echo $"{ }"', 'E15:')
+ END
+ call CheckLegacyAndVim9Success(lines)
+
+ let lines =<< trim END
+ call assert_equal('5', $"{({x -> x + 1})(4)}")
+ END
+ call CheckLegacySuccess(lines)
+
+ let lines =<< trim END
+ call assert_equal('5', $"{((x) => x + 1)(4)}")
+ call assert_fails('echo $"{ # foo }"', 'E1279:')
+ END
+ call CheckDefAndScriptSuccess(lines)
+endfunc
+
" vim: shiftwidth=2 sts=2 expandtab
diff --git a/test/old/testdir/test_let.vim b/test/old/testdir/test_let.vim
index 6fbf305624..0d84164274 100644
--- a/test/old/testdir/test_let.vim
+++ b/test/old/testdir/test_let.vim
@@ -1,5 +1,7 @@
" Tests for the :let command.
+source vim9.vim
+
func Test_let()
" Test to not autoload when assigning. It causes internal error.
set runtimepath+=./sautest
@@ -385,7 +387,18 @@ END
call assert_equal(['Text', 'with', 'indent'], text)
endfunc
-" Test for the setting a variable using the heredoc syntax
+func Test_let_interpolated()
+ call assert_equal('{text}', $'{{text}}')
+ call assert_equal('{{text}}', $'{{{{text}}}}')
+ let text = 'text'
+ call assert_equal('text{{', $'{text .. "{{"}')
+ call assert_equal('text{{', $"{text .. '{{'}")
+ call assert_equal('text{{', $'{text .. '{{'}')
+ call assert_equal('text{{', $"{text .. "{{"}")
+endfunc
+
+" Test for the setting a variable using the heredoc syntax.
+" Keep near the end, this messes up highlighting.
func Test_let_heredoc()
let var1 =<< END
Some sample text
@@ -493,4 +506,109 @@ END
call assert_equal([' x', ' \y', ' z'], [a, b, c])
endfunc
+" Test for evaluating Vim expressions in a heredoc using {expr}
+" Keep near the end, this messes up highlighting.
+func Test_let_heredoc_eval()
+ let str = ''
+ let code =<< trim eval END
+ let a = {5 + 10}
+ let b = {min([10, 6])} + {max([4, 6])}
+ {str}
+ let c = "abc{str}d"
+ END
+ call assert_equal(['let a = 15', 'let b = 6 + 6', '', 'let c = "abcd"'], code)
+
+ let $TESTVAR = "Hello"
+ let code =<< eval trim END
+ let s = "{$TESTVAR}"
+ END
+ call assert_equal(['let s = "Hello"'], code)
+
+ let code =<< eval END
+ let s = "{$TESTVAR}"
+END
+ call assert_equal([' let s = "Hello"'], code)
+
+ let a = 10
+ let data =<< eval END
+{a}
+END
+ call assert_equal(['10'], data)
+
+ let x = 'X'
+ let code =<< eval trim END
+ let a = {{abc}}
+ let b = {x}
+ let c = {{
+ END
+ call assert_equal(['let a = {abc}', 'let b = X', 'let c = {'], code)
+
+ let code = 'xxx'
+ let code =<< eval trim END
+ let n = {5 +
+ 6}
+ END
+ call assert_equal('xxx', code)
+
+ let code =<< eval trim END
+ let n = {min([1, 2]} + {max([3, 4])}
+ END
+ call assert_equal('xxx', code)
+
+ let lines =<< trim LINES
+ let text =<< eval trim END
+ let b = {
+ END
+ LINES
+ call CheckScriptFailure(lines, 'E1279:')
+
+ let lines =<< trim LINES
+ let text =<< eval trim END
+ let b = {abc
+ END
+ LINES
+ call CheckScriptFailure(lines, 'E1279:')
+
+ let lines =<< trim LINES
+ let text =<< eval trim END
+ let b = {}
+ END
+ LINES
+ call CheckScriptFailure(lines, 'E15:')
+
+ " skipped heredoc
+ if 0
+ let msg =<< trim eval END
+ n is: {n}
+ END
+ endif
+
+ " Test for sourcing a script containing a heredoc with invalid expression.
+ " Variable assignment should fail, if expression evaluation fails
+ new
+ let g:Xvar = 'test'
+ let g:b = 10
+ let lines =<< trim END
+ let Xvar =<< eval CODE
+ let a = 1
+ let b = {5+}
+ let c = 2
+ CODE
+ let g:Count += 1
+ END
+ call setline(1, lines)
+ let g:Count = 0
+ call assert_fails('source', 'E15:')
+ call assert_equal(1, g:Count)
+ call setline(3, 'let b = {abc}')
+ call assert_fails('source', 'E121:')
+ call assert_equal(2, g:Count)
+ call setline(3, 'let b = {abc} + {min([9, 4])} + 2')
+ call assert_fails('source', 'E121:')
+ call assert_equal(3, g:Count)
+ call assert_equal('test', g:Xvar)
+ call assert_equal(10, g:b)
+ bw!
+endfunc
+
" vim: shiftwidth=2 sts=2 expandtab
diff --git a/test/old/testdir/test_scriptnames.vim b/test/old/testdir/test_scriptnames.vim
index d804684722..06ae305ab7 100644
--- a/test/old/testdir/test_scriptnames.vim
+++ b/test/old/testdir/test_scriptnames.vim
@@ -35,9 +35,7 @@ func Test_getscriptinfo()
source Xscript
let l = getscriptinfo()
call assert_match('Xscript$', l[-1].name)
- " Nvim does not support interpolated strings yet.
- " call assert_equal(g:loaded_script_id, $"<SNR>{l[-1].sid}_")
- call assert_equal(g:loaded_script_id, '<SNR>' . l[-1].sid . '_')
+ call assert_equal(g:loaded_script_id, $"<SNR>{l[-1].sid}_")
call delete('Xscript')
endfunc