diff options
author | zeertzjq <zeertzjq@outlook.com> | 2023-04-14 21:08:00 +0800 |
---|---|---|
committer | zeertzjq <zeertzjq@outlook.com> | 2023-04-15 17:59:32 +0800 |
commit | bacb5021d4eff33c67eb659fb01125b2abcacd79 (patch) | |
tree | 553983c2a337c8183899bd3f33abb84033bce5fc | |
parent | 2cf8f01e7d0469b592bacecd5f224b4fe3149a62 (diff) | |
download | rneovim-bacb5021d4eff33c67eb659fb01125b2abcacd79.tar.gz rneovim-bacb5021d4eff33c67eb659fb01125b2abcacd79.tar.bz2 rneovim-bacb5021d4eff33c67eb659fb01125b2abcacd79.zip |
vim-patch:8.2.4883: string interpolation only works in heredoc
Problem: String interpolation only works in heredoc.
Solution: Support interpolated strings. Use syntax for heredoc consistent
with strings, similar to C#. (closes vim/vim#10327)
https://github.com/vim/vim/commit/2eaef106e4a7fc9dc74a7e672b5f550ec1f9786e
Cherry-pick Test_Debugger_breakadd_expr() from Vim.
Co-authored-by: LemonBoy <thatlemon@gmail.com>
-rw-r--r-- | runtime/doc/eval.txt | 20 | ||||
-rw-r--r-- | src/nvim/eval.c | 33 | ||||
-rw-r--r-- | src/nvim/eval/vars.c | 81 | ||||
-rw-r--r-- | src/nvim/globals.h | 5 | ||||
-rw-r--r-- | test/old/testdir/test_debugger.vim | 33 | ||||
-rw-r--r-- | test/old/testdir/test_expr.vim | 56 | ||||
-rw-r--r-- | test/old/testdir/test_let.vim | 59 |
7 files changed, 233 insertions, 54 deletions
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index c8eea03f5f..518a190d3c 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -1407,6 +1407,26 @@ to be doubled. These two commands are equivalent: > ------------------------------------------------------------------------------ +interpolated-string *interp-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. + +To include an opening brace '{' or closing brace '}' in the string content +double it. + +Examples: > + let your_name = input("What's your name? ") + echo $"Hello, {your_name}!" + echo $"The square root of 9 is {sqrt(9)}" + +------------------------------------------------------------------------------ option *expr-option* *E112* *E113* &option option value, local value if possible diff --git a/src/nvim/eval.c b/src/nvim/eval.c index b240c36977..f8a9326703 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -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. @@ -4053,6 +4058,32 @@ static int eval_lit_string(char **arg, typval_T *rettv, int evaluate) return OK; } +int eval_interp_string(char **arg, typval_T *rettv, int evaluate) +{ + // *arg is on the '$' character. + (*arg)++; + + rettv->v_type = VAR_STRING; + + typval_T tv; + int ret; + if (**arg == '"') { + ret = eval_string(arg, &tv, evaluate); + } else { + ret = eval_lit_string(arg, &tv, evaluate); + } + + if (ret == FAIL || !evaluate) { + return ret; + } + + rettv->vval.v_string = eval_all_expr_in_str(tv.vval.v_string); + + tv_clear(&tv); + + return rettv->vval.v_string != NULL ? OK : FAIL; +} + /// @return the function name of the partial. char *partial_name(partial_T *pt) FUNC_ATTR_PURE diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index a8d1e01152..b86c49fd98 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -53,50 +53,73 @@ 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 all the Vim expressions (`=expr`) in string "str" and return the +/// Evaluate all the Vim expressions ({expr}) in string "str" and return the /// resulting string. The caller must free the returned string. -static char *eval_all_expr_in_str(char *str) +char *eval_all_expr_in_str(char *str) { garray_T ga; ga_init(&ga, 1, 80); char *p = str; - // Look for `=expr`, evaluate the expression and replace `=expr` with the - // result. while (*p != NUL) { - char *s = p; - while (*p != NUL && (*p != '`' || p[1] != '=')) { - p++; + 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; } - ga_concat_len(&ga, s, (size_t)(p - s)); + + // Append the literal part. + ga_concat_len(&ga, lit_start, (size_t)(p - lit_start)); + if (*p == NUL) { - break; // no backtick expression found + break; } - s = p; - p += 2; // skip `= - int status = *p == NUL ? OK : skip_expr(&p, NULL); - if (status == FAIL || *p != '`') { - // invalid expression or missing ending backtick - if (status != FAIL) { - emsg(_("E1083: Missing backtick")); - } - xfree(ga.ga_data); + if (escaped_brace) { + // Skip the second brace. + ++p; + continue; + } + + // Skip the opening {. + char *block_start = ++p; + char *block_end = block_start; + if (*block_start != NUL && skip_expr(&block_end, NULL) == FAIL) { + ga_clear(&ga); return NULL; } - s += 2; // skip `= - char save_c = *p; - *p = NUL; - char *exprval = eval_to_string(s, true); - *p = save_c; - p++; - if (exprval == NULL) { - // expression evaluation failed - xfree(ga.ga_data); + block_end = skipwhite(block_end); + // The block must be closed by a }. + if (*block_end != '}') { + semsg(_(e_missing_close_curly_str), str); + ga_clear(&ga); return NULL; } - ga_concat(&ga, exprval); - xfree(exprval); + char save_c = *block_end; + *block_end = NUL; + char *expr_val = eval_to_string(block_start, true); + *block_end = save_c; + if (expr_val == NULL) { + ga_clear(&ga); + return NULL; + } + ga_concat(&ga, expr_val); + xfree(expr_val); + + p = block_end + 1; } ga_append(&ga, NUL); 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_expr.vim b/test/old/testdir/test_expr.vim index c8b7fde100..86e720a1ae 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\x7b123}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 915bba2314..8f7121935e 100644 --- a/test/old/testdir/test_let.vim +++ b/test/old/testdir/test_let.vim @@ -387,6 +387,17 @@ END call assert_equal(['Text', 'with', 'indent'], text) endfunc +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 .. '{{'}") + " FIXME: should not need to escape quotes in the expression + 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() @@ -496,72 +507,72 @@ END call assert_equal([' x', ' \y', ' z'], [a, b, c]) endfunc -" Test for evaluating Vim expressions in a heredoc using `=expr` +" 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" + 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`" + let s = "{$TESTVAR}" END call assert_equal(['let s = "Hello"'], code) let code =<< eval END - let s = "`=$TESTVAR`" + let s = "{$TESTVAR}" END call assert_equal([' let s = "Hello"'], code) let a = 10 let data =<< eval END -`=a` +{a} END call assert_equal(['10'], data) let x = 'X' let code =<< eval trim END - let a = `abc` - let b = `=x` - let c = ` + let a = {{abc}} + let b = {x} + let c = {{ END - call assert_equal(['let a = `abc`', 'let b = X', 'let c = `'], code) + call assert_equal(['let a = {abc}', 'let b = X', 'let c = {'], code) let code = 'xxx' let code =<< eval trim END - let n = `=5 + - 6` + let n = {5 + + 6} END call assert_equal('xxx', code) let code =<< eval trim END - let n = `=min([1, 2]` + `=max([3, 4])` + 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 = `= + let b = { END LINES - call CheckScriptFailure(lines, 'E1083:') + call CheckScriptFailure(lines, 'E1279:') let lines =<< trim LINES let text =<< eval trim END - let b = `=abc + let b = {abc END LINES - call CheckScriptFailure(lines, 'E1083:') + call CheckScriptFailure(lines, 'E1279:') let lines =<< trim LINES let text =<< eval trim END - let b = `=` + let b = {} END LINES call CheckScriptFailure(lines, 'E15:') @@ -569,7 +580,7 @@ END " skipped heredoc if 0 let msg =<< trim eval END - n is: `=n` + n is: {n} END endif @@ -581,7 +592,7 @@ END let lines =<< trim END let Xvar =<< eval CODE let a = 1 - let b = `=5+` + let b = {5+} let c = 2 CODE let g:Count += 1 @@ -590,10 +601,10 @@ END let g:Count = 0 call assert_fails('source', 'E15:') call assert_equal(1, g:Count) - call setline(3, 'let b = `=abc`') + 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 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) |