diff options
-rw-r--r-- | runtime/doc/options.txt | 46 | ||||
-rw-r--r-- | src/nvim/api/keysets.lua | 2 | ||||
-rw-r--r-- | src/nvim/api/win_config.c | 103 | ||||
-rw-r--r-- | src/nvim/buffer_defs.h | 19 | ||||
-rw-r--r-- | src/nvim/eval.c | 13 | ||||
-rw-r--r-- | src/nvim/eval.c.orig | 9995 | ||||
-rw-r--r-- | src/nvim/eval/funcs.c | 3 | ||||
-rw-r--r-- | src/nvim/eval/vars.c | 14 | ||||
-rw-r--r-- | src/nvim/generators/gen_options.lua | 20 | ||||
-rw-r--r-- | src/nvim/keycodes.c | 27 | ||||
-rw-r--r-- | src/nvim/keycodes.h | 27 | ||||
-rw-r--r-- | src/nvim/map.c | 2 | ||||
-rw-r--r-- | src/nvim/map.h | 2 | ||||
-rw-r--r-- | src/nvim/ops.c | 244 | ||||
-rw-r--r-- | src/nvim/ops.h | 13 | ||||
-rw-r--r-- | src/nvim/option.c | 6 | ||||
-rw-r--r-- | src/nvim/option_defs.h | 2 | ||||
-rw-r--r-- | src/nvim/options.lua | 9 | ||||
-rw-r--r-- | src/nvim/popupmnu.c | 172 | ||||
-rw-r--r-- | src/nvim/screen.c | 53 | ||||
-rw-r--r-- | src/nvim/shada.c | 4 | ||||
-rw-r--r-- | src/nvim/window.c | 5 | ||||
-rw-r--r-- | src/nvim/yankmap.c | 42 | ||||
-rw-r--r-- | src/nvim/yankmap.h | 24 | ||||
-rw-r--r-- | test/functional/ui/highlight_spec.lua | 7 | ||||
-rw-r--r-- | test/symbolic/klee/nvim/keymap.c | 27 |
26 files changed, 10752 insertions, 129 deletions
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 9d03397821..279f0878f1 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -1332,8 +1332,10 @@ A jump table for the options with a short description can be found at |Q_op|. 'colorcolumn' 'cc' string (default "") local to window 'colorcolumn' is a comma-separated list of screen columns that are - highlighted with ColorColumn |hl-ColorColumn|. Useful to align - text. Will make screen redrawing slower. + highlighted with ColorColumn |hl-ColorColumn| and drawn using the + colocol option from 'fillchars'. Useful to align text. Will make + screen redrawing slower. + The screen column can be an absolute number, or a number preceded with '+' or '-', which is added to or subtracted from 'textwidth'. > @@ -2457,6 +2459,7 @@ A jump table for the options with a short description can be found at |Q_op|. diff:c '-' deleted lines of the 'diff' option msgsep:c ' ' message separator 'display' eob:c '~' empty lines at the end of a buffer + colorcol:c ' ' character to display in the colorcolumn Any one that is omitted will fall back to the default. For "stl" and "stlnc" the space will be used when there is highlighting, '^' or '=' @@ -2494,6 +2497,7 @@ A jump table for the options with a short description can be found at |Q_op|. fold:c Folded |hl-Folded| diff:c DiffDelete |hl-DiffDelete| eob:c EndOfBuffer |hl-EndOfBuffer| + colorcol:c EndOfBuffer |hl-ColorColumn| *'fixendofline'* *'fixeol'* *'nofixendofline'* *'nofixeol'* 'fixendofline' 'fixeol' boolean (default on) @@ -6695,6 +6699,44 @@ A jump table for the options with a short description can be found at |Q_op|. written to disk (see |crash-recovery|). Also used for the |CursorHold| autocommand event. + *'userregfun'* *'urf'* +'userregfun' 'urf' string (default "") + global + The option specifies a function to be used to handle any registers + that Neovim does not natively handle. This option unlocks all + characters to be used as registers by the user. + + The 'userregfun' function is called each time a user register is read + from or written to. + + The 'userregfun' function must take the following parameters: + + {action} The action being done on this register (either 'yank' + or 'put' + + {register} The string holding the name of the register. This + is always a single character, though multi-byte + characters are allowed. + + {content} If the action is 'yank' this is the content being + yanked into the register. + + In case the action is 'put', the 'userregfun' function should return + the content to place in that location. + + A very simple example of a 'userregfun' function that behaves exactly + like traditional registers would look like: > + + let s:contents = {} + function! MyUserregFunction(action, register, content) abort + if a:action == "put" + return get(s:contents, a:register, "") + else + let s:contents[a:register] = a:content + endif + endfunction + set userregfun=MyUserregFunction +< *'varsofttabstop'* *'vsts'* 'varsofttabstop' 'vsts' string (default "") local to buffer diff --git a/src/nvim/api/keysets.lua b/src/nvim/api/keysets.lua index 4f4ac40ce9..a764fb069b 100644 --- a/src/nvim/api/keysets.lua +++ b/src/nvim/api/keysets.lua @@ -73,6 +73,8 @@ return { "border"; "style"; "noautocmd"; + "title"; + "title_position"; }; runtime = { "is_lua"; diff --git a/src/nvim/api/win_config.c b/src/nvim/api/win_config.c index 969643eeef..d36c5bfb95 100644 --- a/src/nvim/api/win_config.c +++ b/src/nvim/api/win_config.c @@ -437,9 +437,73 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) } } +static bool parse_title(FloatConfig* out, String s) +{ + // The raw title is going to be at most the length of the string. + char* out_title_raw = xcalloc(sizeof(char), s.size + 1); + size_t out_cursor = 0; + + char* data = s.data; + + size_t out_hlrec_nalloc = 4; + stl_hlrec_t* out_hlrec = xcalloc(sizeof(stl_hlrec_t), out_hlrec_nalloc); + out_hlrec[0].start = (char*) out_title_raw; + out_hlrec[0].userhl = 0; + size_t out_hl_cur = 1; + + char hlbuf[128]; + size_t hlbuf_cur = 0; + + int hl; + + for (size_t i = 0; i < s.size; i ++) { + if (data[i] == '\\' && i < s.size - 1) { + i ++; + out_title_raw[out_cursor++] = data[i]; + } else if ( + data[i] == '%' && + i < s.size - 1 && data[i + 1] == '#') { + i += 2; + while (i < s.size && data[i] != '#') { + if (hlbuf_cur < sizeof(hlbuf) - 1) { + hlbuf[hlbuf_cur ++] = data[i]; + } + i ++; + } + hlbuf[hlbuf_cur++] = 0; + hl = syn_check_group(hlbuf, strlen(hlbuf)); + hlbuf_cur = 0; + + if (out_hl_cur >= out_hlrec_nalloc - 1) { // Leave room for last. + out_hlrec = + xrealloc(out_hlrec, sizeof(stl_hlrec_t) * (out_hlrec_nalloc *= 2)); + } + + out_hlrec[out_hl_cur].start = (out_title_raw + out_cursor); + out_hlrec[out_hl_cur++].userhl = -hl; + } else { + out_title_raw[out_cursor++] = data[i]; + } + } + + out->n_title = out_cursor; + out_title_raw[out_cursor++] = 0; + out_hlrec[out_hl_cur] = (stl_hlrec_t) { 0, 0 }; + out->title_hl = out_hlrec; + out->title = out_title_raw; + + return true; +} + static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig, bool reconf, bool new_win, Error *err) { + xfree(fconfig->title); + xfree(fconfig->title_hl); + fconfig->title_hl = NULL; + fconfig->n_title = 0; + fconfig->title = NULL; + bool has_relative = false, relative_is_win = false; if (config->relative.type == kObjectTypeString) { // ignore empty string, to match nvim_win_get_config @@ -634,5 +698,44 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig, } } + if (HAS_KEY(config->title)) { + if (!parse_title(fconfig, config->title.data.string)) { + api_set_error(err, kErrorTypeValidation, + "Invalid value of 'title' key."); + goto free_and_fail; + } + } else if (config->title.type == kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, "'title' must be a string"); + return false; + } + + if (HAS_KEY(config->title_position)) { + if (config->title_position.type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, + "Invalid value of 'title_position' key"); + goto free_and_fail; + } + + if (striequal(config->title_position.data.string.data, "left")) { + fconfig->title_pos = kTitleLeft; + } else if (striequal(config->title_position.data.string.data, "center")) { + fconfig->title_pos = kTitleCenter; + } else if (striequal(config->title_position.data.string.data, "right")) { + fconfig->title_pos = kTitleRight; + } else { + api_set_error(err, kErrorTypeValidation, + "Invalid value for 'title_position'"); + goto free_and_fail; + } + } + return true; + +free_and_fail: + xfree(fconfig->title); + xfree(fconfig->title_hl); + fconfig->n_title = 0; + fconfig->title_hl = NULL; + fconfig->title = NULL; + return false; } diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 3ef5ea7e02..b7f66e6dba 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -712,6 +712,7 @@ struct file_buffer { char_u *b_p_cfu; ///< 'completefunc' char_u *b_p_ofu; ///< 'omnifunc' char_u *b_p_tfu; ///< 'tagfunc' + char_u *b_p_urf; ///< 'userregfunc' int b_p_eol; ///< 'endofline' int b_p_fixeol; ///< 'fixendofline' int b_p_et; ///< 'expandtab' @@ -1097,6 +1098,12 @@ typedef enum { kWinStyleMinimal, /// Minimal UI: no number column, eob markers, etc } WinStyle; +typedef enum { + kTitleLeft = 0, + kTitleCenter, + kTitleRight +} TitlePosition; + typedef struct { Window window; lpos_T bufpos; @@ -1114,6 +1121,11 @@ typedef struct { int border_hl_ids[8]; int border_attr[8]; bool noautocmd; + + stl_hlrec_t* title_hl; + char* title; + size_t n_title; + TitlePosition title_pos; } FloatConfig; #define FLOAT_CONFIG_INIT ((FloatConfig){ .height = 0, .width = 0, \ @@ -1123,7 +1135,11 @@ typedef struct { .focusable = true, \ .zindex = kZIndexFloatDefault, \ .style = kWinStyleUnused, \ - .noautocmd = false }) + .noautocmd = false, \ + .title_hl = NULL, \ + .title = NULL, \ + .n_title = 0, \ + .title_pos = kTitleLeft}) // Structure to store last cursor position and topline. Used by check_lnums() // and reset_lnums(). @@ -1257,6 +1273,7 @@ struct window_S { int diff; int msgsep; int eob; + int colorcol; } w_p_fcs_chars; /* diff --git a/src/nvim/eval.c b/src/nvim/eval.c index fc627ae5db..9cc92cb62e 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -3165,13 +3165,14 @@ static int eval7(char **arg, typval_T *rettv, int evaluate, int want_string) // Register contents: @r. case '@': ++*arg; + int regname = mb_cptr2char_adv((const char_u**) arg); if (evaluate) { rettv->v_type = VAR_STRING; - rettv->vval.v_string = get_reg_contents(**arg, kGRegExprSrc); - } - if (**arg != NUL) { - ++*arg; + rettv->vval.v_string = get_reg_contents(regname, kGRegExprSrc); } + // if (**arg != NUL) { + // ++*arg; + // } break; // nested expression: (expression). @@ -4272,7 +4273,7 @@ bool garbage_collect(bool testing) // registers (ShaDa additional data) { - const void *reg_iter = NULL; + iter_register_T reg_iter = ITER_REGISTER_NULL; do { yankreg_T reg; char name = NUL; @@ -4281,7 +4282,7 @@ bool garbage_collect(bool testing) if (name != NUL) { ABORTING(set_ref_dict)(reg.additional_data, copyID); } - } while (reg_iter != NULL); + } while (reg_iter != ITER_REGISTER_NULL); } // global marks (ShaDa additional data) diff --git a/src/nvim/eval.c.orig b/src/nvim/eval.c.orig new file mode 100644 index 0000000000..9bfb2cd7d6 --- /dev/null +++ b/src/nvim/eval.c.orig @@ -0,0 +1,9995 @@ +// This is an open source non-commercial project. Dear PVS-Studio, please check +// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + +/* + * eval.c: Expression evaluation. + */ + +#include <math.h> +#include <stdlib.h> + +#include "auto/config.h" + +#ifdef HAVE_LOCALE_H +# include <locale.h> +#endif + +#include "nvim/ascii.h" +#include "nvim/buffer.h" +#include "nvim/change.h" +#include "nvim/channel.h" +#include "nvim/charset.h" +#include "nvim/cursor.h" +#include "nvim/edit.h" +#include "nvim/eval.h" +#include "nvim/eval/encode.h" +#include "nvim/eval/executor.h" +#include "nvim/eval/gc.h" +#include "nvim/eval/typval.h" +#include "nvim/eval/userfunc.h" +#include "nvim/eval/vars.h" +#include "nvim/ex_cmds2.h" +#include "nvim/ex_getln.h" +#include "nvim/ex_session.h" +#include "nvim/fileio.h" +#include "nvim/getchar.h" +#include "nvim/highlight_group.h" +#include "nvim/lua/executor.h" +#include "nvim/mark.h" +#include "nvim/memline.h" +#include "nvim/move.h" +#include "nvim/ops.h" +#include "nvim/option.h" +#include "nvim/os/input.h" +#include "nvim/os/shell.h" +#include "nvim/path.h" +#include "nvim/quickfix.h" +#include "nvim/regexp.h" +#include "nvim/screen.h" +#include "nvim/search.h" +#include "nvim/sign.h" +#include "nvim/syntax.h" +#include "nvim/ui.h" +#include "nvim/ui_compositor.h" +#include "nvim/undo.h" +#include "nvim/version.h" +#include "nvim/window.h" + +// TODO(ZyX-I): Remove DICT_MAXNEST, make users be non-recursive instead + +#define DICT_MAXNEST 100 // maximum nesting of lists and dicts + +static char *e_missbrac = N_("E111: Missing ']'"); +static char *e_dictrange = N_("E719: Cannot use [:] with a Dictionary"); +static char *e_nowhitespace + = N_("E274: No white space allowed before parenthesis"); +static char *e_write2 = N_("E80: Error while writing: %s"); +static char *e_string_list_or_blob_required = N_("E1098: String, List or Blob required"); + +static char * const namespace_char = "abglstvw"; + +/// Variable used for g: +static ScopeDictDictItem globvars_var; + +/* + * Old Vim variables such as "v:version" are also available without the "v:". + * Also in functions. We need a special hashtable for them. + */ +static hashtab_T compat_hashtab; + +/// Used for checking if local variables or arguments used in a lambda. +bool *eval_lavars_used = NULL; + +/* + * Array to hold the hashtab with variables local to each sourced script. + * Each item holds a variable (nameless) that points to the dict_T. + */ +typedef struct { + ScopeDictDictItem sv_var; + dict_T sv_dict; +} scriptvar_T; + +static garray_T ga_scripts = { 0, 0, sizeof(scriptvar_T *), 4, NULL }; +#define SCRIPT_SV(id) (((scriptvar_T **)ga_scripts.ga_data)[(id) - 1]) +#define SCRIPT_VARS(id) (SCRIPT_SV(id)->sv_dict.dv_hashtab) + +static int echo_attr = 0; // attributes used for ":echo" + +// The names of packages that once were loaded are remembered. +static garray_T ga_loaded = { 0, 0, sizeof(char *), 4, NULL }; + +/* + * Info used by a ":for" loop. + */ +typedef struct { + int fi_semicolon; // TRUE if ending in '; var]' + int fi_varcount; // nr of variables in the list + listwatch_T fi_lw; // keep an eye on the item used. + list_T *fi_list; // list being used + int fi_bi; // index of blob + blob_T *fi_blob; // blob being used + char *fi_string; // copy of string being used + int fi_byte_idx; // byte index in fi_string +} forinfo_T; + +// values for vv_flags: +#define VV_COMPAT 1 // compatible, also used without "v:" +#define VV_RO 2 // read-only +#define VV_RO_SBX 4 // read-only in the sandbox + +#define VV(idx, name, type, flags) \ + [idx] = { \ + .vv_name = (name), \ + .vv_di = { \ + .di_tv = { .v_type = (type) }, \ + .di_flags = 0, \ + .di_key = { 0 }, \ + }, \ + .vv_flags = (flags), \ + } + +#define VIMVAR_KEY_LEN 16 // Maximum length of the key of v:variables + +// Array to hold the value of v: variables. +// The value is in a dictitem, so that it can also be used in the v: scope. +// The reason to use this table anyway is for very quick access to the +// variables with the VV_ defines. +static struct vimvar { + char *vv_name; ///< Name of the variable, without v:. + TV_DICTITEM_STRUCT(VIMVAR_KEY_LEN + 1) vv_di; ///< Value and name for key (max 16 chars). + char vv_flags; ///< Flags: #VV_COMPAT, #VV_RO, #VV_RO_SBX. +} vimvars[] = +{ + // VV_ tails differing from upcased string literals: + // VV_CC_FROM "charconvert_from" + // VV_CC_TO "charconvert_to" + // VV_SEND_SERVER "servername" + // VV_REG "register" + // VV_OP "operator" + VV(VV_COUNT, "count", VAR_NUMBER, VV_RO), + VV(VV_COUNT1, "count1", VAR_NUMBER, VV_RO), + VV(VV_PREVCOUNT, "prevcount", VAR_NUMBER, VV_RO), + VV(VV_ERRMSG, "errmsg", VAR_STRING, 0), + VV(VV_WARNINGMSG, "warningmsg", VAR_STRING, 0), + VV(VV_STATUSMSG, "statusmsg", VAR_STRING, 0), + VV(VV_SHELL_ERROR, "shell_error", VAR_NUMBER, VV_RO), + VV(VV_THIS_SESSION, "this_session", VAR_STRING, 0), + VV(VV_VERSION, "version", VAR_NUMBER, VV_COMPAT + VV_RO), + VV(VV_LNUM, "lnum", VAR_NUMBER, VV_RO_SBX), + VV(VV_TERMRESPONSE, "termresponse", VAR_STRING, VV_RO), + VV(VV_FNAME, "fname", VAR_STRING, VV_RO), + VV(VV_LANG, "lang", VAR_STRING, VV_RO), + VV(VV_LC_TIME, "lc_time", VAR_STRING, VV_RO), + VV(VV_CTYPE, "ctype", VAR_STRING, VV_RO), + VV(VV_CC_FROM, "charconvert_from", VAR_STRING, VV_RO), + VV(VV_CC_TO, "charconvert_to", VAR_STRING, VV_RO), + VV(VV_FNAME_IN, "fname_in", VAR_STRING, VV_RO), + VV(VV_FNAME_OUT, "fname_out", VAR_STRING, VV_RO), + VV(VV_FNAME_NEW, "fname_new", VAR_STRING, VV_RO), + VV(VV_FNAME_DIFF, "fname_diff", VAR_STRING, VV_RO), + VV(VV_CMDARG, "cmdarg", VAR_STRING, VV_RO), + VV(VV_FOLDSTART, "foldstart", VAR_NUMBER, VV_RO_SBX), + VV(VV_FOLDEND, "foldend", VAR_NUMBER, VV_RO_SBX), + VV(VV_FOLDDASHES, "folddashes", VAR_STRING, VV_RO_SBX), + VV(VV_FOLDLEVEL, "foldlevel", VAR_NUMBER, VV_RO_SBX), + VV(VV_PROGNAME, "progname", VAR_STRING, VV_RO), + VV(VV_SEND_SERVER, "servername", VAR_STRING, VV_RO), + VV(VV_DYING, "dying", VAR_NUMBER, VV_RO), + VV(VV_EXCEPTION, "exception", VAR_STRING, VV_RO), + VV(VV_THROWPOINT, "throwpoint", VAR_STRING, VV_RO), + VV(VV_REG, "register", VAR_STRING, VV_RO), + VV(VV_CMDBANG, "cmdbang", VAR_NUMBER, VV_RO), + VV(VV_INSERTMODE, "insertmode", VAR_STRING, VV_RO), + VV(VV_VAL, "val", VAR_UNKNOWN, VV_RO), + VV(VV_KEY, "key", VAR_UNKNOWN, VV_RO), + VV(VV_PROFILING, "profiling", VAR_NUMBER, VV_RO), + VV(VV_FCS_REASON, "fcs_reason", VAR_STRING, VV_RO), + VV(VV_FCS_CHOICE, "fcs_choice", VAR_STRING, 0), + VV(VV_BEVAL_BUFNR, "beval_bufnr", VAR_NUMBER, VV_RO), + VV(VV_BEVAL_WINNR, "beval_winnr", VAR_NUMBER, VV_RO), + VV(VV_BEVAL_WINID, "beval_winid", VAR_NUMBER, VV_RO), + VV(VV_BEVAL_LNUM, "beval_lnum", VAR_NUMBER, VV_RO), + VV(VV_BEVAL_COL, "beval_col", VAR_NUMBER, VV_RO), + VV(VV_BEVAL_TEXT, "beval_text", VAR_STRING, VV_RO), + VV(VV_SCROLLSTART, "scrollstart", VAR_STRING, 0), + VV(VV_SWAPNAME, "swapname", VAR_STRING, VV_RO), + VV(VV_SWAPCHOICE, "swapchoice", VAR_STRING, 0), + VV(VV_SWAPCOMMAND, "swapcommand", VAR_STRING, VV_RO), + VV(VV_CHAR, "char", VAR_STRING, 0), + VV(VV_MOUSE_WIN, "mouse_win", VAR_NUMBER, 0), + VV(VV_MOUSE_WINID, "mouse_winid", VAR_NUMBER, 0), + VV(VV_MOUSE_LNUM, "mouse_lnum", VAR_NUMBER, 0), + VV(VV_MOUSE_COL, "mouse_col", VAR_NUMBER, 0), + VV(VV_OP, "operator", VAR_STRING, VV_RO), + VV(VV_SEARCHFORWARD, "searchforward", VAR_NUMBER, 0), + VV(VV_HLSEARCH, "hlsearch", VAR_NUMBER, 0), + VV(VV_OLDFILES, "oldfiles", VAR_LIST, 0), + VV(VV_WINDOWID, "windowid", VAR_NUMBER, VV_RO_SBX), + VV(VV_PROGPATH, "progpath", VAR_STRING, VV_RO), + VV(VV_COMPLETED_ITEM, "completed_item", VAR_DICT, 0), + VV(VV_OPTION_NEW, "option_new", VAR_STRING, VV_RO), + VV(VV_OPTION_OLD, "option_old", VAR_STRING, VV_RO), + VV(VV_OPTION_OLDLOCAL, "option_oldlocal", VAR_STRING, VV_RO), + VV(VV_OPTION_OLDGLOBAL, "option_oldglobal", VAR_STRING, VV_RO), + VV(VV_OPTION_COMMAND, "option_command", VAR_STRING, VV_RO), + VV(VV_OPTION_TYPE, "option_type", VAR_STRING, VV_RO), + VV(VV_ERRORS, "errors", VAR_LIST, 0), + VV(VV_FALSE, "false", VAR_BOOL, VV_RO), + VV(VV_TRUE, "true", VAR_BOOL, VV_RO), + VV(VV_NULL, "null", VAR_SPECIAL, VV_RO), + VV(VV_NUMBERMAX, "numbermax", VAR_NUMBER, VV_RO), + VV(VV_NUMBERMIN, "numbermin", VAR_NUMBER, VV_RO), + VV(VV_NUMBERSIZE, "numbersize", VAR_NUMBER, VV_RO), + VV(VV_VIM_DID_ENTER, "vim_did_enter", VAR_NUMBER, VV_RO), + VV(VV_TESTING, "testing", VAR_NUMBER, 0), + VV(VV_TYPE_NUMBER, "t_number", VAR_NUMBER, VV_RO), + VV(VV_TYPE_STRING, "t_string", VAR_NUMBER, VV_RO), + VV(VV_TYPE_FUNC, "t_func", VAR_NUMBER, VV_RO), + VV(VV_TYPE_LIST, "t_list", VAR_NUMBER, VV_RO), + VV(VV_TYPE_DICT, "t_dict", VAR_NUMBER, VV_RO), + VV(VV_TYPE_FLOAT, "t_float", VAR_NUMBER, VV_RO), + VV(VV_TYPE_BOOL, "t_bool", VAR_NUMBER, VV_RO), + VV(VV_TYPE_BLOB, "t_blob", VAR_NUMBER, VV_RO), + VV(VV_EVENT, "event", VAR_DICT, VV_RO), + VV(VV_ECHOSPACE, "echospace", VAR_NUMBER, VV_RO), + VV(VV_ARGV, "argv", VAR_LIST, VV_RO), + VV(VV_COLLATE, "collate", VAR_STRING, VV_RO), + VV(VV_EXITING, "exiting", VAR_NUMBER, VV_RO), + // Neovim + VV(VV_STDERR, "stderr", VAR_NUMBER, VV_RO), + VV(VV_MSGPACK_TYPES, "msgpack_types", VAR_DICT, VV_RO), + VV(VV__NULL_STRING, "_null_string", VAR_STRING, VV_RO), + VV(VV__NULL_LIST, "_null_list", VAR_LIST, VV_RO), + VV(VV__NULL_DICT, "_null_dict", VAR_DICT, VV_RO), + VV(VV__NULL_BLOB, "_null_blob", VAR_BLOB, VV_RO), + VV(VV_LUA, "lua", VAR_PARTIAL, VV_RO), +}; +#undef VV + +// shorthand +#define vv_type vv_di.di_tv.v_type +#define vv_nr vv_di.di_tv.vval.v_number +#define vv_bool vv_di.di_tv.vval.v_bool +#define vv_special vv_di.di_tv.vval.v_special +#define vv_float vv_di.di_tv.vval.v_float +#define vv_str vv_di.di_tv.vval.v_string +#define vv_list vv_di.di_tv.vval.v_list +#define vv_dict vv_di.di_tv.vval.v_dict +#define vv_blob vv_di.di_tv.vval.v_blob +#define vv_partial vv_di.di_tv.vval.v_partial +#define vv_tv vv_di.di_tv + +/// Variable used for v: +static ScopeDictDictItem vimvars_var; + +static partial_T *vvlua_partial; + +/// v: hashtab +#define vimvarht vimvardict.dv_hashtab + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "eval.c.generated.h" +#endif + +static uint64_t last_timer_id = 1; +static PMap(uint64_t) timers = MAP_INIT; + +static const char *const msgpack_type_names[] = { + [kMPNil] = "nil", + [kMPBoolean] = "boolean", + [kMPInteger] = "integer", + [kMPFloat] = "float", + [kMPString] = "string", + [kMPBinary] = "binary", + [kMPArray] = "array", + [kMPMap] = "map", + [kMPExt] = "ext", +}; +const list_T *eval_msgpack_type_lists[] = { + [kMPNil] = NULL, + [kMPBoolean] = NULL, + [kMPInteger] = NULL, + [kMPFloat] = NULL, + [kMPString] = NULL, + [kMPBinary] = NULL, + [kMPArray] = NULL, + [kMPMap] = NULL, + [kMPExt] = NULL, +}; + +dict_T *get_v_event(save_v_event_T *sve) +{ + dict_T *v_event = get_vim_var_dict(VV_EVENT); + + if (v_event->dv_hashtab.ht_used > 0) { + // recursive use of v:event, save, make empty and restore later + sve->sve_did_save = true; + sve->sve_hashtab = v_event->dv_hashtab; + hash_init(&v_event->dv_hashtab); + } else { + sve->sve_did_save = false; + } + return v_event; +} + +void restore_v_event(dict_T *v_event, save_v_event_T *sve) +{ + tv_dict_free_contents(v_event); + if (sve->sve_did_save) { + v_event->dv_hashtab = sve->sve_hashtab; + } else { + hash_init(&v_event->dv_hashtab); + } +} + +/// @return "n1" divided by "n2", taking care of dividing by zero. +varnumber_T num_divide(varnumber_T n1, varnumber_T n2) + FUNC_ATTR_CONST FUNC_ATTR_WARN_UNUSED_RESULT +{ + varnumber_T result; + + if (n2 == 0) { // give an error message? + if (n1 == 0) { + result = VARNUMBER_MIN; // similar to NaN + } else if (n1 < 0) { + result = -VARNUMBER_MAX; + } else { + result = VARNUMBER_MAX; + } + } else { + result = n1 / n2; + } + + return result; +} + +/// @return "n1" modulus "n2", taking care of dividing by zero. +varnumber_T num_modulus(varnumber_T n1, varnumber_T n2) + FUNC_ATTR_CONST FUNC_ATTR_WARN_UNUSED_RESULT +{ + // Give an error when n2 is 0? + return (n2 == 0) ? 0 : (n1 % n2); +} + +/// Initialize the global and v: variables. +void eval_init(void) +{ + vimvars[VV_VERSION].vv_nr = VIM_VERSION_100; + + struct vimvar *p; + + init_var_dict(&globvardict, &globvars_var, VAR_DEF_SCOPE); + init_var_dict(&vimvardict, &vimvars_var, VAR_SCOPE); + vimvardict.dv_lock = VAR_FIXED; + hash_init(&compat_hashtab); + func_init(); + + for (size_t i = 0; i < ARRAY_SIZE(vimvars); i++) { + p = &vimvars[i]; + assert(STRLEN(p->vv_name) <= VIMVAR_KEY_LEN); + STRCPY(p->vv_di.di_key, p->vv_name); + if (p->vv_flags & VV_RO) { + p->vv_di.di_flags = DI_FLAGS_RO | DI_FLAGS_FIX; + } else if (p->vv_flags & VV_RO_SBX) { + p->vv_di.di_flags = DI_FLAGS_RO_SBX | DI_FLAGS_FIX; + } else { + p->vv_di.di_flags = DI_FLAGS_FIX; + } + + // add to v: scope dict, unless the value is not always available + if (p->vv_type != VAR_UNKNOWN) { + hash_add(&vimvarht, p->vv_di.di_key); + } + if (p->vv_flags & VV_COMPAT) { + // add to compat scope dict + hash_add(&compat_hashtab, p->vv_di.di_key); + } + } + vimvars[VV_VERSION].vv_nr = VIM_VERSION_100; + + dict_T *const msgpack_types_dict = tv_dict_alloc(); + for (size_t i = 0; i < ARRAY_SIZE(msgpack_type_names); i++) { + list_T *const type_list = tv_list_alloc(0); + tv_list_set_lock(type_list, VAR_FIXED); + tv_list_ref(type_list); + dictitem_T *const di = tv_dict_item_alloc(msgpack_type_names[i]); + di->di_flags |= DI_FLAGS_RO|DI_FLAGS_FIX; + di->di_tv = (typval_T) { + .v_type = VAR_LIST, + .vval = { .v_list = type_list, }, + }; + eval_msgpack_type_lists[i] = type_list; + if (tv_dict_add(msgpack_types_dict, di) == FAIL) { + // There must not be duplicate items in this dictionary by definition. + abort(); + } + } + msgpack_types_dict->dv_lock = VAR_FIXED; + + set_vim_var_dict(VV_MSGPACK_TYPES, msgpack_types_dict); + set_vim_var_dict(VV_COMPLETED_ITEM, tv_dict_alloc_lock(VAR_FIXED)); + + set_vim_var_dict(VV_EVENT, tv_dict_alloc_lock(VAR_FIXED)); + set_vim_var_list(VV_ERRORS, tv_list_alloc(kListLenUnknown)); + set_vim_var_nr(VV_STDERR, CHAN_STDERR); + set_vim_var_nr(VV_SEARCHFORWARD, 1L); + set_vim_var_nr(VV_HLSEARCH, 1L); + set_vim_var_nr(VV_COUNT1, 1); + set_vim_var_nr(VV_TYPE_NUMBER, VAR_TYPE_NUMBER); + set_vim_var_nr(VV_TYPE_STRING, VAR_TYPE_STRING); + set_vim_var_nr(VV_TYPE_FUNC, VAR_TYPE_FUNC); + set_vim_var_nr(VV_TYPE_LIST, VAR_TYPE_LIST); + set_vim_var_nr(VV_TYPE_DICT, VAR_TYPE_DICT); + set_vim_var_nr(VV_TYPE_FLOAT, VAR_TYPE_FLOAT); + set_vim_var_nr(VV_TYPE_BOOL, VAR_TYPE_BOOL); + set_vim_var_nr(VV_TYPE_BLOB, VAR_TYPE_BLOB); + + set_vim_var_bool(VV_FALSE, kBoolVarFalse); + set_vim_var_bool(VV_TRUE, kBoolVarTrue); + set_vim_var_special(VV_NULL, kSpecialVarNull); + set_vim_var_nr(VV_NUMBERMAX, VARNUMBER_MAX); + set_vim_var_nr(VV_NUMBERMIN, VARNUMBER_MIN); + set_vim_var_nr(VV_NUMBERSIZE, sizeof(varnumber_T) * 8); + set_vim_var_special(VV_EXITING, kSpecialVarNull); + + set_vim_var_nr(VV_ECHOSPACE, sc_col - 1); + + vimvars[VV_LUA].vv_type = VAR_PARTIAL; + vvlua_partial = xcalloc(1, sizeof(partial_T)); + vimvars[VV_LUA].vv_partial = vvlua_partial; + // this value shouldn't be printed, but if it is, do not crash + vvlua_partial->pt_name = xmallocz(0); + vvlua_partial->pt_refcount++; + + set_reg_var(0); // default for v:register is not 0 but '"' +} + +#if defined(EXITFREE) +void eval_clear(void) +{ + struct vimvar *p; + + for (size_t i = 0; i < ARRAY_SIZE(vimvars); i++) { + p = &vimvars[i]; + if (p->vv_di.di_tv.v_type == VAR_STRING) { + XFREE_CLEAR(p->vv_str); + } else if (p->vv_di.di_tv.v_type == VAR_LIST) { + tv_list_unref(p->vv_list); + p->vv_list = NULL; + } + } + hash_clear(&vimvarht); + hash_init(&vimvarht); // garbage_collect() will access it + hash_clear(&compat_hashtab); + + free_scriptnames(); +# ifdef HAVE_WORKING_LIBINTL + free_locales(); +# endif + + // global variables + vars_clear(&globvarht); + + // autoloaded script names + ga_clear_strings(&ga_loaded); + + /* Script-local variables. First clear all the variables and in a second + * loop free the scriptvar_T, because a variable in one script might hold + * a reference to the whole scope of another script. */ + for (int i = 1; i <= ga_scripts.ga_len; ++i) { + vars_clear(&SCRIPT_VARS(i)); + } + for (int i = 1; i <= ga_scripts.ga_len; ++i) { + xfree(SCRIPT_SV(i)); + } + ga_clear(&ga_scripts); + + // unreferenced lists and dicts + (void)garbage_collect(false); + + // functions not garbage collected + free_all_functions(); +} + +#endif + +/// Set an internal variable to a string value. Creates the variable if it does +/// not already exist. +void set_internal_string_var(const char *name, char *value) + FUNC_ATTR_NONNULL_ARG(1) +{ + typval_T tv = { + .v_type = VAR_STRING, + .vval.v_string = value, + }; + + set_var(name, strlen(name), &tv, true); +} + +static lval_T *redir_lval = NULL; +static garray_T redir_ga; // Only valid when redir_lval is not NULL. +static char *redir_endp = NULL; +static char *redir_varname = NULL; + +/// Start recording command output to a variable +/// +/// @param append append to an existing variable +/// +/// @return OK if successfully completed the setup. FAIL otherwise. +int var_redir_start(char *name, int append) +{ + int save_emsg; + int err; + typval_T tv; + + // Catch a bad name early. + if (!eval_isnamec1(*name)) { + emsg(_(e_invarg)); + return FAIL; + } + + // Make a copy of the name, it is used in redir_lval until redir ends. + redir_varname = xstrdup(name); + + redir_lval = xcalloc(1, sizeof(lval_T)); + + // The output is stored in growarray "redir_ga" until redirection ends. + ga_init(&redir_ga, (int)sizeof(char), 500); + + // Parse the variable name (can be a dict or list entry). + redir_endp = get_lval(redir_varname, NULL, redir_lval, false, false, + 0, FNE_CHECK_START); + if (redir_endp == NULL || redir_lval->ll_name == NULL + || *redir_endp != NUL) { + clear_lval(redir_lval); + if (redir_endp != NULL && *redir_endp != NUL) { + // Trailing characters are present after the variable name + emsg(_(e_trailing)); + } else { + emsg(_(e_invarg)); + } + redir_endp = NULL; // don't store a value, only cleanup + var_redir_stop(); + return FAIL; + } + + /* check if we can write to the variable: set it to or append an empty + * string */ + save_emsg = did_emsg; + did_emsg = FALSE; + tv.v_type = VAR_STRING; + tv.vval.v_string = ""; + if (append) { + set_var_lval(redir_lval, redir_endp, &tv, true, false, "."); + } else { + set_var_lval(redir_lval, redir_endp, &tv, true, false, "="); + } + clear_lval(redir_lval); + err = did_emsg; + did_emsg |= save_emsg; + if (err) { + redir_endp = NULL; // don't store a value, only cleanup + var_redir_stop(); + return FAIL; + } + + return OK; +} + +/// Append "value[value_len]" to the variable set by var_redir_start(). +/// The actual appending is postponed until redirection ends, because the value +/// appended may in fact be the string we write to, changing it may cause freed +/// memory to be used: +/// :redir => foo +/// :let foo +/// :redir END +void var_redir_str(char *value, int value_len) +{ + int len; + + if (redir_lval == NULL) { + return; + } + + if (value_len == -1) { + len = (int)STRLEN(value); // Append the entire string + } else { + len = value_len; // Append only "value_len" characters + } + + ga_grow(&redir_ga, len); + memmove((char *)redir_ga.ga_data + redir_ga.ga_len, value, (size_t)len); + redir_ga.ga_len += len; +} + +/// Stop redirecting command output to a variable. +/// Frees the allocated memory. +void var_redir_stop(void) +{ + typval_T tv; + + if (redir_lval != NULL) { + // If there was no error: assign the text to the variable. + if (redir_endp != NULL) { + ga_append(&redir_ga, NUL); // Append the trailing NUL. + tv.v_type = VAR_STRING; + tv.vval.v_string = redir_ga.ga_data; + // Call get_lval() again, if it's inside a Dict or List it may + // have changed. + redir_endp = get_lval(redir_varname, NULL, redir_lval, + false, false, 0, FNE_CHECK_START); + if (redir_endp != NULL && redir_lval->ll_name != NULL) { + set_var_lval(redir_lval, redir_endp, &tv, false, false, "."); + } + clear_lval(redir_lval); + } + + // free the collected output + XFREE_CLEAR(redir_ga.ga_data); + + XFREE_CLEAR(redir_lval); + } + XFREE_CLEAR(redir_varname); +} + +int eval_charconvert(const char *const enc_from, const char *const enc_to, + const char *const fname_from, const char *const fname_to) +{ + bool err = false; + + set_vim_var_string(VV_CC_FROM, enc_from, -1); + set_vim_var_string(VV_CC_TO, enc_to, -1); + set_vim_var_string(VV_FNAME_IN, fname_from, -1); + set_vim_var_string(VV_FNAME_OUT, fname_to, -1); + if (eval_to_bool((char *)p_ccv, &err, NULL, false)) { + err = true; + } + set_vim_var_string(VV_CC_FROM, NULL, -1); + set_vim_var_string(VV_CC_TO, NULL, -1); + set_vim_var_string(VV_FNAME_IN, NULL, -1); + set_vim_var_string(VV_FNAME_OUT, NULL, -1); + + if (err) { + return FAIL; + } + return OK; +} + +int eval_printexpr(const char *const fname, const char *const args) +{ + bool err = false; + + set_vim_var_string(VV_FNAME_IN, fname, -1); + set_vim_var_string(VV_CMDARG, args, -1); + if (eval_to_bool((char *)p_pexpr, &err, NULL, false)) { + err = true; + } + set_vim_var_string(VV_FNAME_IN, NULL, -1); + set_vim_var_string(VV_CMDARG, NULL, -1); + + if (err) { + os_remove(fname); + return FAIL; + } + return OK; +} + +void eval_diff(const char *const origfile, const char *const newfile, const char *const outfile) +{ + bool err = false; + + set_vim_var_string(VV_FNAME_IN, origfile, -1); + set_vim_var_string(VV_FNAME_NEW, newfile, -1); + set_vim_var_string(VV_FNAME_OUT, outfile, -1); + (void)eval_to_bool((char *)p_dex, &err, NULL, false); + set_vim_var_string(VV_FNAME_IN, NULL, -1); + set_vim_var_string(VV_FNAME_NEW, NULL, -1); + set_vim_var_string(VV_FNAME_OUT, NULL, -1); +} + +void eval_patch(const char *const origfile, const char *const difffile, const char *const outfile) +{ + bool err = false; + + set_vim_var_string(VV_FNAME_IN, origfile, -1); + set_vim_var_string(VV_FNAME_DIFF, difffile, -1); + set_vim_var_string(VV_FNAME_OUT, outfile, -1); + (void)eval_to_bool((char *)p_pex, &err, NULL, false); + set_vim_var_string(VV_FNAME_IN, NULL, -1); + set_vim_var_string(VV_FNAME_DIFF, NULL, -1); + set_vim_var_string(VV_FNAME_OUT, NULL, -1); +} + +/// Top level evaluation function, returning a boolean. +/// Sets "error" to TRUE if there was an error. +/// +/// @param skip only parse, don't execute +/// +/// @return TRUE or FALSE. +int eval_to_bool(char *arg, bool *error, char **nextcmd, int skip) +{ + typval_T tv; + bool retval = false; + + if (skip) { + emsg_skip++; + } + if (eval0(arg, &tv, nextcmd, !skip) == FAIL) { + *error = true; + } else { + *error = false; + if (!skip) { + retval = (tv_get_number_chk(&tv, error) != 0); + tv_clear(&tv); + } + } + if (skip) { + emsg_skip--; + } + + return retval; +} + +/// Call eval1() and give an error message if not done at a lower level. +static int eval1_emsg(char **arg, typval_T *rettv, bool evaluate) + FUNC_ATTR_NONNULL_ARG(1, 2) +{ + const char *const start = *arg; + const int did_emsg_before = did_emsg; + const int called_emsg_before = called_emsg; + + const int ret = eval1(arg, rettv, evaluate); + if (ret == FAIL) { + // Report the invalid expression unless the expression evaluation has + // been cancelled due to an aborting error, an interrupt, or an + // exception, or we already gave a more specific error. + // Also check called_emsg for when using assert_fails(). + if (!aborting() + && did_emsg == did_emsg_before + && called_emsg == called_emsg_before) { + semsg(_(e_invexpr2), start); + } + } + return ret; +} + +/// @return whether a typval is a valid expression to pass to eval_expr_typval() +/// or eval_expr_to_bool(). An empty string returns false; +bool eval_expr_valid_arg(const typval_T *const tv) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_CONST +{ + return tv->v_type != VAR_UNKNOWN + && (tv->v_type != VAR_STRING || (tv->vval.v_string != NULL && *tv->vval.v_string != NUL)); +} + +int eval_expr_typval(const typval_T *expr, typval_T *argv, int argc, typval_T *rettv) + FUNC_ATTR_NONNULL_ARG(1, 2, 4) +{ + funcexe_T funcexe = FUNCEXE_INIT; + + if (expr->v_type == VAR_FUNC) { + const char *const s = expr->vval.v_string; + if (s == NULL || *s == NUL) { + return FAIL; + } + funcexe.evaluate = true; + if (call_func(s, -1, rettv, argc, argv, &funcexe) == FAIL) { + return FAIL; + } + } else if (expr->v_type == VAR_PARTIAL) { + partial_T *const partial = expr->vval.v_partial; + const char *const s = partial_name(partial); + if (s == NULL || *s == NUL) { + return FAIL; + } + funcexe.evaluate = true; + funcexe.partial = partial; + if (call_func(s, -1, rettv, argc, argv, &funcexe) == FAIL) { + return FAIL; + } + } else { + char buf[NUMBUFLEN]; + char *s = (char *)tv_get_string_buf_chk(expr, buf); + if (s == NULL) { + return FAIL; + } + s = skipwhite(s); + if (eval1_emsg(&s, rettv, true) == FAIL) { + return FAIL; + } + if (*skipwhite(s) != NUL) { // check for trailing chars after expr + tv_clear(rettv); + semsg(_(e_invexpr2), s); + return FAIL; + } + } + return OK; +} + +/// Like eval_to_bool() but using a typval_T instead of a string. +/// Works for string, funcref and partial. +bool eval_expr_to_bool(const typval_T *expr, bool *error) + FUNC_ATTR_NONNULL_ARG(1, 2) +{ + typval_T argv, rettv; + + if (eval_expr_typval(expr, &argv, 0, &rettv) == FAIL) { + *error = true; + return false; + } + const bool res = (tv_get_number_chk(&rettv, error) != 0); + tv_clear(&rettv); + return res; +} + +/// Top level evaluation function, returning a string +/// +/// @param[in] arg String to evaluate. +/// @param nextcmd Pointer to the start of the next Ex command. +/// @param[in] skip If true, only do parsing to nextcmd without reporting +/// errors or actually evaluating anything. +/// +/// @return [allocated] string result of evaluation or NULL in case of error or +/// when skipping. +char *eval_to_string_skip(const char *arg, const char **nextcmd, const bool skip) + FUNC_ATTR_MALLOC FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_WARN_UNUSED_RESULT +{ + typval_T tv; + char *retval; + + if (skip) { + emsg_skip++; + } + if (eval0((char *)arg, &tv, (char **)nextcmd, !skip) == FAIL || skip) { + retval = NULL; + } else { + retval = xstrdup(tv_get_string(&tv)); + tv_clear(&tv); + } + if (skip) { + emsg_skip--; + } + + return retval; +} + +/// Skip over an expression at "*pp". +/// +/// @return FAIL for an error, OK otherwise. +int skip_expr(char **pp) +{ + typval_T rettv; + + *pp = skipwhite(*pp); + return eval1(pp, &rettv, false); +} + +/// Top level evaluation function, returning a string. +/// +/// @param convert when true convert a List into a sequence of lines and convert +/// a Float to a String. +/// +/// @return pointer to allocated memory, or NULL for failure. +char *eval_to_string(char *arg, char **nextcmd, bool convert) +{ + typval_T tv; + char *retval; + garray_T ga; + + if (eval0(arg, &tv, nextcmd, true) == FAIL) { + retval = NULL; + } else { + if (convert && tv.v_type == VAR_LIST) { + ga_init(&ga, (int)sizeof(char), 80); + if (tv.vval.v_list != NULL) { + tv_list_join(&ga, tv.vval.v_list, "\n"); + if (tv_list_len(tv.vval.v_list) > 0) { + ga_append(&ga, NL); + } + } + ga_append(&ga, NUL); + retval = (char *)ga.ga_data; + } else if (convert && tv.v_type == VAR_FLOAT) { + char numbuf[NUMBUFLEN]; + vim_snprintf(numbuf, NUMBUFLEN, "%g", tv.vval.v_float); + retval = xstrdup(numbuf); + } else { + retval = xstrdup(tv_get_string(&tv)); + } + tv_clear(&tv); + } + + return retval; +} + +/// Call eval_to_string() without using current local variables and using +/// textlock. +/// +/// @param use_sandbox when TRUE, use the sandbox. +char *eval_to_string_safe(char *arg, char **nextcmd, int use_sandbox) +{ + char *retval; + funccal_entry_T funccal_entry; + + save_funccal(&funccal_entry); + if (use_sandbox) { + sandbox++; + } + textlock++; + retval = eval_to_string(arg, nextcmd, false); + if (use_sandbox) { + sandbox--; + } + textlock--; + restore_funccal(); + return retval; +} + +/// Top level evaluation function, returning a number. +/// Evaluates "expr" silently. +/// +/// @return -1 for an error. +varnumber_T eval_to_number(char *expr) +{ + typval_T rettv; + varnumber_T retval; + char *p = skipwhite(expr); + + ++emsg_off; + + if (eval1(&p, &rettv, true) == FAIL) { + retval = -1; + } else { + retval = tv_get_number_chk(&rettv, NULL); + tv_clear(&rettv); + } + --emsg_off; + + return retval; +} + +/// Top level evaluation function. +/// +/// @return an allocated typval_T with the result or +/// NULL when there is an error. +typval_T *eval_expr(char *arg) +{ + typval_T *tv = xmalloc(sizeof(*tv)); + if (eval0(arg, tv, NULL, true) == FAIL) { + XFREE_CLEAR(tv); + } + return tv; +} + +/// List Vim variables. +void list_vim_vars(int *first) +{ + list_hashtable_vars(&vimvarht, "v:", false, first); +} + +/// List script-local variables, if there is a script. +void list_script_vars(int *first) +{ + if (current_sctx.sc_sid > 0 && current_sctx.sc_sid <= ga_scripts.ga_len) { + list_hashtable_vars(&SCRIPT_VARS(current_sctx.sc_sid), "s:", false, first); + } +} + +bool is_vimvarht(const hashtab_T *ht) +{ + return ht == &vimvarht; +} + +bool is_compatht(const hashtab_T *ht) +{ + return ht == &compat_hashtab; +} + +/// Prepare v: variable "idx" to be used. +/// Save the current typeval in "save_tv". +/// When not used yet add the variable to the v: hashtable. +void prepare_vimvar(int idx, typval_T *save_tv) +{ + *save_tv = vimvars[idx].vv_tv; + if (vimvars[idx].vv_type == VAR_UNKNOWN) { + hash_add(&vimvarht, vimvars[idx].vv_di.di_key); + } +} + +/// Restore v: variable "idx" to typeval "save_tv". +/// When no longer defined, remove the variable from the v: hashtable. +void restore_vimvar(int idx, typval_T *save_tv) +{ + hashitem_T *hi; + + vimvars[idx].vv_tv = *save_tv; + if (vimvars[idx].vv_type == VAR_UNKNOWN) { + hi = hash_find(&vimvarht, (char *)vimvars[idx].vv_di.di_key); + if (HASHITEM_EMPTY(hi)) { + internal_error("restore_vimvar()"); + } else { + hash_remove(&vimvarht, hi); + } + } +} + +/// If there is a window for "curbuf", make it the current window. +void find_win_for_curbuf(void) +{ + for (wininfo_T *wip = curbuf->b_wininfo; wip != NULL; wip = wip->wi_next) { + if (wip->wi_win != NULL) { + curwin = wip->wi_win; + break; + } + } +} + +/// Evaluate an expression to a list with suggestions. +/// For the "expr:" part of 'spellsuggest'. +/// +/// @return NULL when there is an error. +list_T *eval_spell_expr(char *badword, char *expr) +{ + typval_T save_val; + typval_T rettv; + list_T *list = NULL; + char *p = skipwhite(expr); + + // Set "v:val" to the bad word. + prepare_vimvar(VV_VAL, &save_val); + vimvars[VV_VAL].vv_type = VAR_STRING; + vimvars[VV_VAL].vv_str = badword; + if (p_verbose == 0) { + ++emsg_off; + } + + if (eval1(&p, &rettv, true) == OK) { + if (rettv.v_type != VAR_LIST) { + tv_clear(&rettv); + } else { + list = rettv.vval.v_list; + } + } + + if (p_verbose == 0) { + --emsg_off; + } + restore_vimvar(VV_VAL, &save_val); + + return list; +} + +/// Get spell word from an entry from spellsuggest=expr: +/// +/// Entry in question is supposed to be a list (to be checked by the caller) +/// with two items: a word and a score represented as an unsigned number +/// (whether it actually is unsigned is not checked). +/// +/// Used to get the good word and score from the eval_spell_expr() result. +/// +/// @param[in] list List to get values from. +/// @param[out] ret_word Suggested word. Not initialized if return value is +/// -1. +/// +/// @return -1 in case of error, score otherwise. +int get_spellword(list_T *const list, const char **ret_word) +{ + if (tv_list_len(list) != 2) { + emsg(_("E5700: Expression from 'spellsuggest' must yield lists with " + "exactly two values")); + return -1; + } + *ret_word = tv_list_find_str(list, 0); + if (*ret_word == NULL) { + return -1; + } + return (int)tv_list_find_nr(list, -1, NULL); +} + +// Call some vim script function and return the result in "*rettv". +// Uses argv[0] to argv[argc-1] for the function arguments. argv[argc] +// should have type VAR_UNKNOWN. +// +// @return OK or FAIL. +int call_vim_function(const char *func, int argc, typval_T *argv, typval_T *rettv) + FUNC_ATTR_NONNULL_ALL +{ + int ret; + int len = (int)STRLEN(func); + partial_T *pt = NULL; + + if (len >= 6 && !memcmp(func, "v:lua.", 6)) { + func += 6; + len = check_luafunc_name(func, false); + if (len == 0) { + ret = FAIL; + goto fail; + } + pt = vvlua_partial; + } + + rettv->v_type = VAR_UNKNOWN; // tv_clear() uses this. + funcexe_T funcexe = FUNCEXE_INIT; + funcexe.firstline = curwin->w_cursor.lnum; + funcexe.lastline = curwin->w_cursor.lnum; + funcexe.evaluate = true; + funcexe.partial = pt; + ret = call_func(func, len, rettv, argc, argv, &funcexe); + +fail: + if (ret == FAIL) { + tv_clear(rettv); + } + + return ret; +} +/// Call Vim script function and return the result as a number +/// +/// @param[in] func Function name. +/// @param[in] argc Number of arguments. +/// @param[in] argv Array with typval_T arguments. +/// +/// @return -1 when calling function fails, result of function otherwise. +varnumber_T call_func_retnr(const char *func, int argc, typval_T *argv) + FUNC_ATTR_NONNULL_ALL +{ + typval_T rettv; + varnumber_T retval; + + if (call_vim_function((char *)func, argc, argv, &rettv) == FAIL) { + return -1; + } + retval = tv_get_number_chk(&rettv, NULL); + tv_clear(&rettv); + return retval; +} +/// Call Vim script function and return the result as a string +/// +/// @param[in] func Function name. +/// @param[in] argc Number of arguments. +/// @param[in] argv Array with typval_T arguments. +/// +/// @return [allocated] NULL when calling function fails, allocated string +/// otherwise. +char *call_func_retstr(const char *const func, int argc, typval_T *argv) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_MALLOC +{ + typval_T rettv; + // All arguments are passed as strings, no conversion to number. + if (call_vim_function(func, argc, argv, &rettv) + == FAIL) { + return NULL; + } + + char *const retval = xstrdup(tv_get_string(&rettv)); + tv_clear(&rettv); + return retval; +} +/// Call Vim script function and return the result as a List +/// +/// @param[in] func Function name. +/// @param[in] argc Number of arguments. +/// @param[in] argv Array with typval_T arguments. +/// +/// @return [allocated] NULL when calling function fails or return tv is not a +/// List, allocated List otherwise. +void *call_func_retlist(const char *func, int argc, typval_T *argv) + FUNC_ATTR_NONNULL_ALL +{ + typval_T rettv; + + // All arguments are passed as strings, no conversion to number. + if (call_vim_function((char *)func, argc, argv, &rettv) == FAIL) { + return NULL; + } + + if (rettv.v_type != VAR_LIST) { + tv_clear(&rettv); + return NULL; + } + + return rettv.vval.v_list; +} + +/// Prepare profiling for entering a child or something else that is not +/// counted for the script/function itself. +/// Should always be called in pair with prof_child_exit(). +/// +/// @param tm place to store waittime +void prof_child_enter(proftime_T *tm) +{ + funccall_T *fc = get_current_funccal(); + + if (fc != NULL && fc->func->uf_profiling) { + fc->prof_child = profile_start(); + } + + script_prof_save(tm); +} + +/// Take care of time spent in a child. +/// Should always be called after prof_child_enter(). +/// +/// @param tm where waittime was stored +void prof_child_exit(proftime_T *tm) +{ + funccall_T *fc = get_current_funccal(); + + if (fc != NULL && fc->func->uf_profiling) { + fc->prof_child = profile_end(fc->prof_child); + // don't count waiting time + fc->prof_child = profile_sub_wait(*tm, fc->prof_child); + fc->func->uf_tm_children = + profile_add(fc->func->uf_tm_children, fc->prof_child); + fc->func->uf_tml_children = + profile_add(fc->func->uf_tml_children, fc->prof_child); + } + script_prof_restore(tm); +} + +/// Evaluate 'foldexpr'. Returns the foldlevel, and any character preceding +/// it in "*cp". Doesn't give error messages. +int eval_foldexpr(char *arg, int *cp) +{ + typval_T tv; + varnumber_T retval; + int use_sandbox = was_set_insecurely(curwin, "foldexpr", OPT_LOCAL); + + ++emsg_off; + if (use_sandbox) { + ++sandbox; + } + ++textlock; + *cp = NUL; + if (eval0(arg, &tv, NULL, true) == FAIL) { + retval = 0; + } else { + // If the result is a number, just return the number. + if (tv.v_type == VAR_NUMBER) { + retval = tv.vval.v_number; + } else if (tv.v_type != VAR_STRING || tv.vval.v_string == NULL) { + retval = 0; + } else { + // If the result is a string, check if there is a non-digit before + // the number. + char *s = tv.vval.v_string; + if (!ascii_isdigit(*s) && *s != '-') { + *cp = (char_u)(*s++); + } + retval = atol(s); + } + tv_clear(&tv); + } + --emsg_off; + if (use_sandbox) { + --sandbox; + } + --textlock; + + return (int)retval; +} + +<<<<<<< HEAD +/// ":cons[t] var = expr1" define constant +/// ":cons[t] [name1, name2, ...] = expr1" define constants unpacking list +/// ":cons[t] [name, ..., ; lastname] = expr" define constants unpacking list +void ex_const(exarg_T *eap) +{ + ex_let_const(eap, true); +} + +/// Get a list of lines from a HERE document. The here document is a list of +/// lines surrounded by a marker. +/// cmd << {marker} +/// {line1} +/// {line2} +/// .... +/// {marker} +/// +/// The {marker} is a string. If the optional 'trim' word is supplied before the +/// marker, then the leading indentation before the lines (matching the +/// indentation in the 'cmd' line) is stripped. +/// +/// @return a List with {lines} or NULL. +static list_T *heredoc_get(exarg_T *eap, char *cmd) +{ + char *marker; + char *p; + int marker_indent_len = 0; + int text_indent_len = 0; + char *text_indent = NULL; + + if (eap->getline == NULL) { + emsg(_("E991: cannot use =<< here")); + return NULL; + } + + // 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++; + } + text_indent_len = -1; + } + + // The marker is the next word. + if (*cmd != NUL && *cmd != '"') { + marker = skipwhite(cmd); + p = (char *)skiptowhite((char_u *)marker); + if (*skipwhite(p) != NUL && *skipwhite(p) != '"') { + emsg(_(e_trailing)); + return NULL; + } + *p = NUL; + if (islower(*marker)) { + emsg(_("E221: Marker cannot start with lower case letter")); + return NULL; + } + } else { + emsg(_("E172: Missing marker")); + return NULL; + } + + list_T *l = tv_list_alloc(0); + for (;;) { + int mi = 0; + int ti = 0; + + char *theline = eap->getline(NUL, eap->cookie, 0, false); + if (theline == NULL) { + semsg(_("E990: Missing end marker '%s'"), marker); + break; + } + + // with "trim": skip the indent matching the :let line to find the + // marker + if (marker_indent_len > 0 + && STRNCMP(theline, *eap->cmdlinep, marker_indent_len) == 0) { + mi = marker_indent_len; + } + if (STRCMP(marker, theline + mi) == 0) { + xfree(theline); + break; + } + if (text_indent_len == -1 && *theline != NUL) { + // set the text indent from the first line. + p = theline; + text_indent_len = 0; + while (ascii_iswhite(*p)) { + p++; + text_indent_len++; + } + text_indent = xstrnsave(theline, (size_t)text_indent_len); + } + // with "trim": skip the indent matching the first line + if (text_indent != NULL) { + for (ti = 0; ti < text_indent_len; ti++) { + if (theline[ti] != text_indent[ti]) { + break; + } + } + } + + tv_list_append_string(l, theline + ti, -1); + xfree(theline); + } + xfree(text_indent); + + return l; +} + +/// ":let" list all variable values +/// ":let var1 var2" list variable values +/// ":let var = expr" assignment command. +/// ":let var += expr" assignment command. +/// ":let var -= expr" assignment command. +/// ":let var *= expr" assignment command. +/// ":let var /= expr" assignment command. +/// ":let var %= expr" assignment command. +/// ":let var .= expr" assignment command. +/// ":let var ..= expr" assignment command. +/// ":let [var1, var2] = expr" unpack list. +/// ":let [name, ..., ; lastname] = expr" unpack list. +void ex_let(exarg_T *eap) +{ + ex_let_const(eap, false); +} + +static void ex_let_const(exarg_T *eap, const bool is_const) +{ + char *arg = eap->arg; + char *expr = NULL; + typval_T rettv; + int i; + int var_count = 0; + int semicolon = 0; + char op[2]; + char *argend; + int first = true; + + argend = (char *)skip_var_list(arg, &var_count, &semicolon); + if (argend == NULL) { + return; + } + if (argend > arg && argend[-1] == '.') { // For var.='str'. + argend--; + } + expr = skipwhite(argend); + if (*expr != '=' && !((vim_strchr("+-*/%.", *expr) != NULL + && expr[1] == '=') || STRNCMP(expr, "..=", 3) == 0)) { + // ":let" without "=": list variables + if (*arg == '[') { + emsg(_(e_invarg)); + } else if (!ends_excmd(*arg)) { + // ":let var1 var2" + arg = (char *)list_arg_vars(eap, (const char *)arg, &first); + } else if (!eap->skip) { + // ":let" + list_glob_vars(&first); + list_buf_vars(&first); + list_win_vars(&first); + list_tab_vars(&first); + list_script_vars(&first); + list_func_vars(&first); + list_vim_vars(&first); + } + eap->nextcmd = (char *)check_nextcmd((char_u *)arg); + } else if (expr[0] == '=' && expr[1] == '<' && expr[2] == '<') { + // HERE document + list_T *l = heredoc_get(eap, expr + 3); + if (l != NULL) { + tv_list_set_ret(&rettv, l); + if (!eap->skip) { + op[0] = '='; + op[1] = NUL; + (void)ex_let_vars(eap->arg, &rettv, false, semicolon, var_count, + is_const, (char *)op); + } + tv_clear(&rettv); + } + } else { + op[0] = '='; + op[1] = NUL; + if (*expr != '=') { + if (vim_strchr("+-*/%.", *expr) != NULL) { + op[0] = *expr; // +=, -=, *=, /=, %= or .= + if (expr[0] == '.' && expr[1] == '.') { // ..= + expr++; + } + } + expr = skipwhite(expr + 2); + } else { + expr = skipwhite(expr + 1); + } + + if (eap->skip) { + ++emsg_skip; + } + i = eval0(expr, &rettv, &eap->nextcmd, !eap->skip); + if (eap->skip) { + if (i != FAIL) { + tv_clear(&rettv); + } + emsg_skip--; + } else if (i != FAIL) { + (void)ex_let_vars(eap->arg, &rettv, false, semicolon, var_count, + is_const, (char *)op); + tv_clear(&rettv); + } + } +} + +/// Assign the typevalue "tv" to the variable or variables at "arg_start". +/// Handles both "var" with any type and "[var, var; var]" with a list type. +/// When "op" is not NULL it points to a string with characters that +/// must appear after the variable(s). Use "+", "-" or "." for add, subtract +/// or concatenate. +/// +/// @param copy copy values from "tv", don't move +/// @param semicolon from skip_var_list() +/// @param var_count from skip_var_list() +/// @param is_const lock variables for :const +/// +/// @return OK or FAIL; +static int ex_let_vars(char *arg_start, typval_T *tv, int copy, int semicolon, int var_count, + int is_const, char *op) +{ + char *arg = arg_start; + typval_T ltv; + + if (*arg != '[') { + /* + * ":let var = expr" or ":for var in list" + */ + if (ex_let_one(arg, tv, copy, is_const, op, op) == NULL) { + return FAIL; + } + return OK; + } + + // ":let [v1, v2] = list" or ":for [v1, v2] in listlist" + if (tv->v_type != VAR_LIST) { + emsg(_(e_listreq)); + return FAIL; + } + list_T *const l = tv->vval.v_list; + + const int len = tv_list_len(l); + if (semicolon == 0 && var_count < len) { + emsg(_("E687: Less targets than List items")); + return FAIL; + } + if (var_count - semicolon > len) { + emsg(_("E688: More targets than List items")); + return FAIL; + } + // List l may actually be NULL, but it should fail with E688 or even earlier + // if you try to do ":let [] = v:_null_list". + assert(l != NULL); + + listitem_T *item = tv_list_first(l); + size_t rest_len = (size_t)tv_list_len(l); + while (*arg != ']') { + arg = skipwhite(arg + 1); + arg = ex_let_one(arg, TV_LIST_ITEM_TV(item), true, is_const, ",;]", op); + if (arg == NULL) { + return FAIL; + } + rest_len--; + + item = TV_LIST_ITEM_NEXT(l, item); + arg = skipwhite(arg); + if (*arg == ';') { + /* Put the rest of the list (may be empty) in the var after ';'. + * Create a new list for this. */ + list_T *const rest_list = tv_list_alloc((ptrdiff_t)rest_len); + while (item != NULL) { + tv_list_append_tv(rest_list, TV_LIST_ITEM_TV(item)); + item = TV_LIST_ITEM_NEXT(l, item); + } + + ltv.v_type = VAR_LIST; + ltv.v_lock = VAR_UNLOCKED; + ltv.vval.v_list = rest_list; + tv_list_ref(rest_list); + + arg = ex_let_one(skipwhite(arg + 1), <v, false, is_const, "]", op); + tv_clear(<v); + if (arg == NULL) { + return FAIL; + } + break; + } else if (*arg != ',' && *arg != ']') { + internal_error("ex_let_vars()"); + return FAIL; + } + } + + return OK; +} + +/// Skip over assignable variable "var" or list of variables "[var, var]". +/// Used for ":let varvar = expr" and ":for varvar in expr". +/// For "[var, var]" increment "*var_count" for each variable. +/// for "[var, var; var]" set "semicolon". +/// +/// @return NULL for an error. +static const char *skip_var_list(const char *arg, int *var_count, int *semicolon) +{ + const char *p; + const char *s; + + if (*arg == '[') { + // "[var, var]": find the matching ']'. + p = arg; + for (;;) { + p = skipwhite(p + 1); // skip whites after '[', ';' or ',' + s = skip_var_one((char *)p); + if (s == p) { + semsg(_(e_invarg2), p); + return NULL; + } + ++*var_count; + + p = skipwhite(s); + if (*p == ']') { + break; + } else if (*p == ';') { + if (*semicolon == 1) { + emsg(_("E452: Double ; in list of variables")); + return NULL; + } + *semicolon = 1; + } else if (*p != ',') { + semsg(_(e_invarg2), p); + return NULL; + } + } + return p + 1; + } else { + return skip_var_one((char *)arg); + } +} + +/// Skip one (assignable) variable name, including @r, $VAR, &option, d.key, +/// l[idx]. +static const char *skip_var_one(const char *arg) +{ + if (*arg == '@' && arg[1] != NUL) { + return arg + 1 + utfc_ptr2len(arg + 1); + } + return (char *)find_name_end(*arg == '$' || *arg == '&' ? arg + 1 : arg, + NULL, NULL, FNE_INCL_BR | FNE_CHECK_START); +} + +/// List variables for hashtab "ht" with prefix "prefix". +/// +/// @param empty if TRUE also list NULL strings as empty strings. +void list_hashtable_vars(hashtab_T *ht, const char *prefix, int empty, int *first) +{ + hashitem_T *hi; + dictitem_T *di; + int todo; + + todo = (int)ht->ht_used; + for (hi = ht->ht_array; todo > 0 && !got_int; ++hi) { + if (!HASHITEM_EMPTY(hi)) { + todo--; + di = TV_DICT_HI2DI(hi); + char buf[IOSIZE]; + + // apply :filter /pat/ to variable name + xstrlcpy(buf, prefix, IOSIZE); + xstrlcat(buf, (char *)di->di_key, IOSIZE); + if (message_filtered((char_u *)buf)) { + continue; + } + + if (empty || di->di_tv.v_type != VAR_STRING + || di->di_tv.vval.v_string != NULL) { + list_one_var(di, prefix, first); + } + } + } +} + +/// List global variables. +static void list_glob_vars(int *first) +{ + list_hashtable_vars(&globvarht, "", true, first); +} + +/// List buffer variables. +static void list_buf_vars(int *first) +{ + list_hashtable_vars(&curbuf->b_vars->dv_hashtab, "b:", true, first); +} + +/// List window variables. +static void list_win_vars(int *first) +{ + list_hashtable_vars(&curwin->w_vars->dv_hashtab, "w:", true, first); +} + +/// List tab page variables. +static void list_tab_vars(int *first) +{ + list_hashtable_vars(&curtab->tp_vars->dv_hashtab, "t:", true, first); +} + +/// List Vim variables. +static void list_vim_vars(int *first) +{ + list_hashtable_vars(&vimvarht, "v:", false, first); +} + +/// List script-local variables, if there is a script. +static void list_script_vars(int *first) +{ + if (current_sctx.sc_sid > 0 && current_sctx.sc_sid <= ga_scripts.ga_len) { + list_hashtable_vars(&SCRIPT_VARS(current_sctx.sc_sid), "s:", false, first); + } +} + +/// List variables in "arg". +static const char *list_arg_vars(exarg_T *eap, const char *arg, int *first) +{ + int error = FALSE; + int len; + const char *name; + const char *name_start; + typval_T tv; + + while (!ends_excmd(*arg) && !got_int) { + if (error || eap->skip) { + arg = find_name_end(arg, NULL, NULL, FNE_INCL_BR | FNE_CHECK_START); + if (!ascii_iswhite(*arg) && !ends_excmd(*arg)) { + emsg_severe = true; + emsg(_(e_trailing)); + break; + } + } else { + // get_name_len() takes care of expanding curly braces + name_start = name = arg; + char *tofree; + len = get_name_len(&arg, &tofree, true, true); + if (len <= 0) { + /* This is mainly to keep test 49 working: when expanding + * curly braces fails overrule the exception error message. */ + if (len < 0 && !aborting()) { + emsg_severe = true; + semsg(_(e_invarg2), arg); + break; + } + error = TRUE; + } else { + if (tofree != NULL) { + name = tofree; + } + if (get_var_tv(name, len, &tv, NULL, true, false) + == FAIL) { + error = true; + } else { + // handle d.key, l[idx], f(expr) + const char *const arg_subsc = arg; + if (handle_subscript(&arg, &tv, true, true, name, &name) == FAIL) { + error = true; + } else { + if (arg == arg_subsc && len == 2 && name[1] == ':') { + switch (*name) { + case 'g': + list_glob_vars(first); break; + case 'b': + list_buf_vars(first); break; + case 'w': + list_win_vars(first); break; + case 't': + list_tab_vars(first); break; + case 'v': + list_vim_vars(first); break; + case 's': + list_script_vars(first); break; + case 'l': + list_func_vars(first); break; + default: + semsg(_("E738: Can't list variables for %s"), name); + } + } else { + char *const s = encode_tv2echo(&tv, NULL); + const char *const used_name = (arg == arg_subsc + ? name + : name_start); + const ptrdiff_t name_size = (used_name == tofree + ? (ptrdiff_t)strlen(used_name) + : (arg - used_name)); + list_one_var_a("", used_name, name_size, + tv.v_type, s == NULL ? "" : s, first); + xfree(s); + } + tv_clear(&tv); + } + } + } + + xfree(tofree); + } + + arg = (const char *)skipwhite(arg); + } + + return arg; +} + +// TODO(ZyX-I): move to eval/ex_cmds + +/// Set one item of `:let var = expr` or `:let [v1, v2] = list` to its value +/// +/// @param[in] arg Start of the variable name. +/// @param[in] tv Value to assign to the variable. +/// @param[in] copy If true, copy value from `tv`. +/// @param[in] endchars Valid characters after variable name or NULL. +/// @param[in] op Operation performed: *op is `+`, `-`, `.` for `+=`, etc. +/// NULL for `=`. +/// +/// @return a pointer to the char just after the var name or NULL in case of +/// error. +static char *ex_let_one(char *arg, typval_T *const tv, const bool copy, const bool is_const, + const char *const endchars, const char *const op) + FUNC_ATTR_NONNULL_ARG(1, 2) FUNC_ATTR_WARN_UNUSED_RESULT +{ + char *arg_end = NULL; + int len; + int opt_flags; + char *tofree = NULL; + + /* + * ":let $VAR = expr": Set environment variable. + */ + if (*arg == '$') { + if (is_const) { + emsg(_("E996: Cannot lock an environment variable")); + return NULL; + } + // Find the end of the name. + arg++; + char *name = arg; + len = get_env_len((const char **)&arg); + if (len == 0) { + semsg(_(e_invarg2), name - 1); + } else { + if (op != NULL && vim_strchr("+-*/%", *op) != NULL) { + semsg(_(e_letwrong), op); + } else if (endchars != NULL + && vim_strchr(endchars, *skipwhite(arg)) == NULL) { + emsg(_(e_letunexp)); + } else if (!check_secure()) { + const char c1 = name[len]; + name[len] = NUL; + const char *p = tv_get_string_chk(tv); + if (p != NULL && op != NULL && *op == '.') { + char *s = vim_getenv(name); + + if (s != NULL) { + tofree = (char *)concat_str((const char_u *)s, (const char_u *)p); + p = (const char *)tofree; + xfree(s); + } + } + if (p != NULL) { + os_setenv(name, p, 1); + if (STRICMP(name, "HOME") == 0) { + init_homedir(); + } else if (didset_vim && STRICMP(name, "VIM") == 0) { + didset_vim = false; + } else if (didset_vimruntime + && STRICMP(name, "VIMRUNTIME") == 0) { + didset_vimruntime = false; + } + arg_end = arg; + } + name[len] = c1; + xfree(tofree); + } + } + // ":let &option = expr": Set option value. + // ":let &l:option = expr": Set local option value. + // ":let &g:option = expr": Set global option value. + } else if (*arg == '&') { + if (is_const) { + emsg(_("E996: Cannot lock an option")); + return NULL; + } + // Find the end of the name. + char *const p = (char *)find_option_end((const char **)&arg, &opt_flags); + if (p == NULL + || (endchars != NULL + && vim_strchr(endchars, *skipwhite(p)) == NULL)) { + emsg(_(e_letunexp)); + } else { + int opt_type; + long numval; + char *stringval = NULL; + const char *s = NULL; + + const char c1 = *p; + *p = NUL; + + varnumber_T n = tv_get_number(tv); + if (tv->v_type != VAR_BOOL && tv->v_type != VAR_SPECIAL) { + s = tv_get_string_chk(tv); // != NULL if number or string. + } + if (s != NULL && op != NULL && *op != '=') { + opt_type = get_option_value(arg, &numval, &stringval, opt_flags); + if ((opt_type == 1 && *op == '.') + || (opt_type == 0 && *op != '.')) { + semsg(_(e_letwrong), op); + s = NULL; // don't set the value + } else { + if (opt_type == 1) { // number + switch (*op) { + case '+': + n = numval + n; break; + case '-': + n = numval - n; break; + case '*': + n = numval * n; break; + case '/': + n = num_divide(numval, n); break; + case '%': + n = num_modulus(numval, n); break; + } + } else if (opt_type == 0 && stringval != NULL) { // string + char *const oldstringval = stringval; + stringval = (char *)concat_str((const char_u *)stringval, + (const char_u *)s); + xfree(oldstringval); + s = stringval; + } + } + } + if (s != NULL || tv->v_type == VAR_BOOL + || tv->v_type == VAR_SPECIAL) { + set_option_value((const char *)arg, n, s, opt_flags); + arg_end = p; + } + *p = c1; + xfree(stringval); + } + // ":let @r = expr": Set register contents. + } else if (*arg == '@') { + if (is_const) { + emsg(_("E996: Cannot lock a register")); + return NULL; + } + arg++; + + int regname = utf_ptr2char(arg); + int mblen = utf_ptr2len(arg); + + if (op != NULL && vim_strchr("+-*/%", *op) != NULL) { + semsg(_(e_letwrong), op); + } else if (endchars != NULL + && vim_strchr(endchars, *skipwhite(arg + mblen)) == NULL) { + emsg(_(e_letunexp)); + } else { + char *s; + + char *ptofree = NULL; + const char *p = tv_get_string_chk(tv); + if (p != NULL && op != NULL && *op == '.') { + s = get_reg_contents(regname == '@' ? '"' : regname, kGRegExprSrc); + if (s != NULL) { + ptofree = (char *)concat_str((char_u *)s, (const char_u *)p); + p = (const char *)ptofree; + xfree(s); + } + } + if (p != NULL) { + + write_reg_contents(regname == '@' ? '"' : regname, + (const char_u *)p, (ssize_t)STRLEN(p), false); + arg_end = arg + mblen; + } + xfree(ptofree); + } + } + /* + * ":let var = expr": Set internal variable. + * ":let {expr} = expr": Idem, name made with curly braces + */ + else if (eval_isnamec1(*arg) || *arg == '{') { + lval_T lv; + + char *const p = get_lval(arg, tv, &lv, false, false, 0, FNE_CHECK_START); + if (p != NULL && lv.ll_name != NULL) { + if (endchars != NULL && vim_strchr(endchars, *skipwhite(p)) == NULL) { + emsg(_(e_letunexp)); + } else { + set_var_lval(&lv, p, tv, copy, is_const, op); + arg_end = p; + } + } + clear_lval(&lv); + } else { + semsg(_(e_invarg2), arg); + } + + return arg_end; +} + +======= +>>>>>>> upstream/master +// TODO(ZyX-I): move to eval/executor + +/// Get an lvalue +/// +/// Lvalue may be +/// - variable: "name", "na{me}" +/// - dictionary item: "dict.key", "dict['key']" +/// - list item: "list[expr]" +/// - list slice: "list[expr:expr]" +/// +/// Indexing only works if trying to use it with an existing List or Dictionary. +/// +/// @param[in] name Name to parse. +/// @param rettv Pointer to the value to be assigned or NULL. +/// @param[out] lp Lvalue definition. When evaluation errors occur `->ll_name` +/// is NULL. +/// @param[in] unlet True if using `:unlet`. This results in slightly +/// different behaviour when something is wrong; must end in +/// space or cmd separator. +/// @param[in] skip True when skipping. +/// @param[in] flags @see GetLvalFlags. +/// @param[in] fne_flags Flags for find_name_end(). +/// +/// @return A pointer to just after the name, including indexes. Returns NULL +/// for a parsing error, but it is still needed to free items in lp. +char *get_lval(char *const name, typval_T *const rettv, lval_T *const lp, const bool unlet, + const bool skip, const int flags, const int fne_flags) + FUNC_ATTR_NONNULL_ARG(1, 3) +{ + dictitem_T *v; + typval_T var1; + typval_T var2; + int empty1 = FALSE; + listitem_T *ni; + hashtab_T *ht = NULL; + int quiet = flags & GLV_QUIET; + + // Clear everything in "lp". + memset(lp, 0, sizeof(lval_T)); + + if (skip) { + // When skipping just find the end of the name. + lp->ll_name = (const char *)name; + return (char *)find_name_end(name, NULL, NULL, + FNE_INCL_BR | fne_flags); + } + + // Find the end of the name. + char *expr_start; + char *expr_end; + char *p = (char *)find_name_end(name, (const char **)&expr_start, + (const char **)&expr_end, + fne_flags); + if (expr_start != NULL) { + // Don't expand the name when we already know there is an error. + if (unlet && !ascii_iswhite(*p) && !ends_excmd(*p) + && *p != '[' && *p != '.') { + emsg(_(e_trailing)); + return NULL; + } + + lp->ll_exp_name = make_expanded_name(name, expr_start, expr_end, p); + lp->ll_name = lp->ll_exp_name; + if (lp->ll_exp_name == NULL) { + // Report an invalid expression in braces, unless the + // expression evaluation has been cancelled due to an + // aborting error, an interrupt, or an exception. + if (!aborting() && !quiet) { + emsg_severe = true; + semsg(_(e_invarg2), name); + return NULL; + } + lp->ll_name_len = 0; + } else { + lp->ll_name_len = strlen(lp->ll_name); + } + } else { + lp->ll_name = (const char *)name; + lp->ll_name_len = (size_t)((const char *)p - lp->ll_name); + } + + // Without [idx] or .key we are done. + if ((*p != '[' && *p != '.') || lp->ll_name == NULL) { + return p; + } + + // Only pass &ht when we would write to the variable, it prevents autoload + // as well. + v = find_var(lp->ll_name, lp->ll_name_len, + (flags & GLV_READ_ONLY) ? NULL : &ht, + flags & GLV_NO_AUTOLOAD); + if (v == NULL && !quiet) { + semsg(_("E121: Undefined variable: %.*s"), + (int)lp->ll_name_len, lp->ll_name); + } + if (v == NULL) { + return NULL; + } + + // Loop until no more [idx] or .key is following. + lp->ll_tv = &v->di_tv; + var1.v_type = VAR_UNKNOWN; + var2.v_type = VAR_UNKNOWN; + while (*p == '[' || (*p == '.' && lp->ll_tv->v_type == VAR_DICT)) { + if (!(lp->ll_tv->v_type == VAR_LIST && lp->ll_tv->vval.v_list != NULL) + && !(lp->ll_tv->v_type == VAR_DICT && lp->ll_tv->vval.v_dict != NULL) + && !(lp->ll_tv->v_type == VAR_BLOB && lp->ll_tv->vval.v_blob != NULL)) { + if (!quiet) { + emsg(_("E689: Can only index a List, Dictionary or Blob")); + } + return NULL; + } + if (lp->ll_range) { + if (!quiet) { + emsg(_("E708: [:] must come last")); + } + return NULL; + } + + int len = -1; + char *key = NULL; + if (*p == '.') { + key = p + 1; + for (len = 0; ASCII_ISALNUM(key[len]) || key[len] == '_'; len++) {} + if (len == 0) { + if (!quiet) { + emsg(_("E713: Cannot use empty key after .")); + } + return NULL; + } + p = key + len; + } else { + // Get the index [expr] or the first index [expr: ]. + p = skipwhite(p + 1); + if (*p == ':') { + empty1 = true; + } else { + empty1 = false; + if (eval1(&p, &var1, true) == FAIL) { // Recursive! + return NULL; + } + if (!tv_check_str(&var1)) { + // Not a number or string. + tv_clear(&var1); + return NULL; + } + p = skipwhite(p); + } + + // Optionally get the second index [ :expr]. + if (*p == ':') { + if (lp->ll_tv->v_type == VAR_DICT) { + if (!quiet) { + emsg(_(e_dictrange)); + } + tv_clear(&var1); + return NULL; + } + if (rettv != NULL + && !(rettv->v_type == VAR_LIST && rettv->vval.v_list != NULL) + && !(rettv->v_type == VAR_BLOB && rettv->vval.v_blob != NULL)) { + if (!quiet) { + emsg(_("E709: [:] requires a List or Blob value")); + } + tv_clear(&var1); + return NULL; + } + p = skipwhite(p + 1); + if (*p == ']') { + lp->ll_empty2 = true; + } else { + lp->ll_empty2 = false; + if (eval1(&p, &var2, true) == FAIL) { // Recursive! + tv_clear(&var1); + return NULL; + } + if (!tv_check_str(&var2)) { + // Not a number or string. + tv_clear(&var1); + tv_clear(&var2); + return NULL; + } + } + lp->ll_range = true; + } else { + lp->ll_range = false; + } + + if (*p != ']') { + if (!quiet) { + emsg(_(e_missbrac)); + } + tv_clear(&var1); + tv_clear(&var2); + return NULL; + } + + // Skip to past ']'. + p++; + } + + if (lp->ll_tv->v_type == VAR_DICT) { + if (len == -1) { + // "[key]": get key from "var1" + key = (char *)tv_get_string(&var1); // is number or string + } + lp->ll_list = NULL; + lp->ll_dict = lp->ll_tv->vval.v_dict; + lp->ll_di = tv_dict_find(lp->ll_dict, (const char *)key, len); + + // When assigning to a scope dictionary check that a function and + // variable name is valid (only variable name unless it is l: or + // g: dictionary). Disallow overwriting a builtin function. + if (rettv != NULL && lp->ll_dict->dv_scope != 0) { + char prevval; + int wrong; + + if (len != -1) { + prevval = key[len]; + key[len] = NUL; + } else { + prevval = 0; // Avoid compiler warning. + } + wrong = ((lp->ll_dict->dv_scope == VAR_DEF_SCOPE + && tv_is_func(*rettv) + && !var_check_func_name((const char *)key, lp->ll_di == NULL)) + || !valid_varname((const char *)key)); + if (len != -1) { + key[len] = prevval; + } + if (wrong) { + return NULL; + } + } + + if (lp->ll_di != NULL && tv_is_luafunc(&lp->ll_di->di_tv) + && len == -1 && rettv == NULL) { + tv_clear(&var1); + semsg(e_illvar, "v:['lua']"); + return NULL; + } + + if (lp->ll_di == NULL) { + // Can't add "v:" or "a:" variable. + if (lp->ll_dict == &vimvardict + || &lp->ll_dict->dv_hashtab == get_funccal_args_ht()) { + semsg(_(e_illvar), name); + tv_clear(&var1); + return NULL; + } + + // Key does not exist in dict: may need to add it. + if (*p == '[' || *p == '.' || unlet) { + if (!quiet) { + semsg(_(e_dictkey), key); + } + tv_clear(&var1); + return NULL; + } + if (len == -1) { + lp->ll_newkey = xstrdup(key); + } else { + lp->ll_newkey = xstrnsave(key, (size_t)len); + } + tv_clear(&var1); + break; + // existing variable, need to check if it can be changed + } else if (!(flags & GLV_READ_ONLY) && var_check_ro(lp->ll_di->di_flags, + (const char *)name, + (size_t)(p - name))) { + tv_clear(&var1); + return NULL; + } + + tv_clear(&var1); + lp->ll_tv = &lp->ll_di->di_tv; + } else if (lp->ll_tv->v_type == VAR_BLOB) { + // Get the number and item for the only or first index of the List. + if (empty1) { + lp->ll_n1 = 0; + } else { + // Is number or string. + lp->ll_n1 = (long)tv_get_number(&var1); + } + tv_clear(&var1); + + const int bloblen = tv_blob_len(lp->ll_tv->vval.v_blob); + if (lp->ll_n1 < 0 || lp->ll_n1 > bloblen + || (lp->ll_range && lp->ll_n1 == bloblen)) { + if (!quiet) { + semsg(_(e_blobidx), (int64_t)lp->ll_n1); + } + tv_clear(&var2); + return NULL; + } + if (lp->ll_range && !lp->ll_empty2) { + lp->ll_n2 = (long)tv_get_number(&var2); + tv_clear(&var2); + if (lp->ll_n2 < 0 || lp->ll_n2 >= bloblen || lp->ll_n2 < lp->ll_n1) { + if (!quiet) { + semsg(_(e_blobidx), (int64_t)lp->ll_n2); + } + return NULL; + } + } + lp->ll_blob = lp->ll_tv->vval.v_blob; + lp->ll_tv = NULL; + break; + } else { + // Get the number and item for the only or first index of the List. + if (empty1) { + lp->ll_n1 = 0; + } else { + // Is number or string. + lp->ll_n1 = (long)tv_get_number(&var1); + } + tv_clear(&var1); + + lp->ll_dict = NULL; + lp->ll_list = lp->ll_tv->vval.v_list; + lp->ll_li = tv_list_find(lp->ll_list, (int)lp->ll_n1); + if (lp->ll_li == NULL) { + if (lp->ll_n1 < 0) { + lp->ll_n1 = 0; + lp->ll_li = tv_list_find(lp->ll_list, (int)lp->ll_n1); + } + } + if (lp->ll_li == NULL) { + tv_clear(&var2); + if (!quiet) { + semsg(_(e_listidx), (int64_t)lp->ll_n1); + } + return NULL; + } + + // May need to find the item or absolute index for the second + // index of a range. + // When no index given: "lp->ll_empty2" is true. + // Otherwise "lp->ll_n2" is set to the second index. + if (lp->ll_range && !lp->ll_empty2) { + lp->ll_n2 = (long)tv_get_number(&var2); // Is number or string. + tv_clear(&var2); + if (lp->ll_n2 < 0) { + ni = tv_list_find(lp->ll_list, (int)lp->ll_n2); + if (ni == NULL) { + if (!quiet) { + semsg(_(e_listidx), (int64_t)lp->ll_n2); + } + return NULL; + } + lp->ll_n2 = tv_list_idx_of_item(lp->ll_list, ni); + } + + // Check that lp->ll_n2 isn't before lp->ll_n1. + if (lp->ll_n1 < 0) { + lp->ll_n1 = tv_list_idx_of_item(lp->ll_list, lp->ll_li); + } + if (lp->ll_n2 < lp->ll_n1) { + if (!quiet) { + semsg(_(e_listidx), (int64_t)lp->ll_n2); + } + return NULL; + } + } + + lp->ll_tv = TV_LIST_ITEM_TV(lp->ll_li); + } + } + + tv_clear(&var1); + return p; +} + +// TODO(ZyX-I): move to eval/executor + +/// Clear lval "lp" that was filled by get_lval(). +void clear_lval(lval_T *lp) +{ + xfree(lp->ll_exp_name); + xfree(lp->ll_newkey); +} + +// TODO(ZyX-I): move to eval/executor + +/// Set a variable that was parsed by get_lval() to "rettv". +/// +/// @param endp points to just after the parsed name. +/// @param op NULL, "+" for "+=", "-" for "-=", "*" for "*=", "/" for "/=", +/// "%" for "%=", "." for ".=" or "=" for "=". +void set_var_lval(lval_T *lp, char *endp, typval_T *rettv, int copy, const bool is_const, + const char *op) +{ + int cc; + listitem_T *ri; + dictitem_T *di; + + if (lp->ll_tv == NULL) { + cc = (char_u)(*endp); + *endp = NUL; + if (lp->ll_blob != NULL) { + if (op != NULL && *op != '=') { + semsg(_(e_letwrong), op); + return; + } + if (var_check_lock(lp->ll_blob->bv_lock, lp->ll_name, TV_CSTRING)) { + return; + } + + if (lp->ll_range && rettv->v_type == VAR_BLOB) { + if (lp->ll_empty2) { + lp->ll_n2 = tv_blob_len(lp->ll_blob) - 1; + } + + if (lp->ll_n2 - lp->ll_n1 + 1 != tv_blob_len(rettv->vval.v_blob)) { + emsg(_("E972: Blob value does not have the right number of bytes")); + return; + } + if (lp->ll_empty2) { + lp->ll_n2 = tv_blob_len(lp->ll_blob); + } + + for (int il = (int)lp->ll_n1, ir = 0; il <= (int)lp->ll_n2; il++) { + tv_blob_set(lp->ll_blob, il, tv_blob_get(rettv->vval.v_blob, ir++)); + } + } else { + bool error = false; + const char val = (char)tv_get_number_chk(rettv, &error); + if (!error) { + garray_T *const gap = &lp->ll_blob->bv_ga; + + // Allow for appending a byte. Setting a byte beyond + // the end is an error otherwise. + if (lp->ll_n1 < gap->ga_len || lp->ll_n1 == gap->ga_len) { + ga_grow(&lp->ll_blob->bv_ga, 1); + tv_blob_set(lp->ll_blob, (int)lp->ll_n1, (char_u)val); + if (lp->ll_n1 == gap->ga_len) { + gap->ga_len++; + } + } + // error for invalid range was already given in get_lval() + } + } + } else if (op != NULL && *op != '=') { + typval_T tv; + + if (is_const) { + emsg(_(e_cannot_mod)); + *endp = (char)cc; + return; + } + + // handle +=, -=, *=, /=, %= and .= + di = NULL; + if (get_var_tv(lp->ll_name, (int)STRLEN(lp->ll_name), + &tv, &di, true, false) == OK) { + if ((di == NULL + || (!var_check_ro(di->di_flags, lp->ll_name, TV_CSTRING) + && !tv_check_lock(&di->di_tv, lp->ll_name, TV_CSTRING))) + && eexe_mod_op(&tv, rettv, op) == OK) { + set_var(lp->ll_name, lp->ll_name_len, &tv, false); + } + tv_clear(&tv); + } + } else { + set_var_const(lp->ll_name, lp->ll_name_len, rettv, copy, is_const); + } + *endp = (char)cc; + } else if (var_check_lock(lp->ll_newkey == NULL + ? lp->ll_tv->v_lock + : lp->ll_tv->vval.v_dict->dv_lock, + lp->ll_name, TV_CSTRING)) { + // Skip + } else if (lp->ll_range) { + listitem_T *ll_li = lp->ll_li; + int ll_n1 = (int)lp->ll_n1; + + if (is_const) { + emsg(_("E996: Cannot lock a range")); + return; + } + + // Check whether any of the list items is locked + for (ri = tv_list_first(rettv->vval.v_list); + ri != NULL && ll_li != NULL;) { + if (var_check_lock(TV_LIST_ITEM_TV(ll_li)->v_lock, lp->ll_name, + TV_CSTRING)) { + return; + } + ri = TV_LIST_ITEM_NEXT(rettv->vval.v_list, ri); + if (ri == NULL || (!lp->ll_empty2 && lp->ll_n2 == ll_n1)) { + break; + } + ll_li = TV_LIST_ITEM_NEXT(lp->ll_list, ll_li); + ll_n1++; + } + + /* + * Assign the List values to the list items. + */ + for (ri = tv_list_first(rettv->vval.v_list); ri != NULL;) { + if (op != NULL && *op != '=') { + eexe_mod_op(TV_LIST_ITEM_TV(lp->ll_li), TV_LIST_ITEM_TV(ri), op); + } else { + tv_clear(TV_LIST_ITEM_TV(lp->ll_li)); + tv_copy(TV_LIST_ITEM_TV(ri), TV_LIST_ITEM_TV(lp->ll_li)); + } + ri = TV_LIST_ITEM_NEXT(rettv->vval.v_list, ri); + if (ri == NULL || (!lp->ll_empty2 && lp->ll_n2 == lp->ll_n1)) { + break; + } + assert(lp->ll_li != NULL); + if (TV_LIST_ITEM_NEXT(lp->ll_list, lp->ll_li) == NULL) { + // Need to add an empty item. + tv_list_append_number(lp->ll_list, 0); + // ll_li may have become invalid after append, don’t use it. + lp->ll_li = tv_list_last(lp->ll_list); // Valid again. + } else { + lp->ll_li = TV_LIST_ITEM_NEXT(lp->ll_list, lp->ll_li); + } + lp->ll_n1++; + } + if (ri != NULL) { + emsg(_("E710: List value has more items than target")); + } else if (lp->ll_empty2 + ? (lp->ll_li != NULL + && TV_LIST_ITEM_NEXT(lp->ll_list, lp->ll_li) != NULL) + : lp->ll_n1 != lp->ll_n2) { + emsg(_("E711: List value has not enough items")); + } + } else { + typval_T oldtv = TV_INITIAL_VALUE; + dict_T *dict = lp->ll_dict; + bool watched = tv_dict_is_watched(dict); + + if (is_const) { + emsg(_("E996: Cannot lock a list or dict")); + return; + } + + // Assign to a List or Dictionary item. + if (lp->ll_newkey != NULL) { + if (op != NULL && *op != '=') { + semsg(_(e_letwrong), op); + return; + } + + // Need to add an item to the Dictionary. + di = tv_dict_item_alloc((const char *)lp->ll_newkey); + if (tv_dict_add(lp->ll_tv->vval.v_dict, di) == FAIL) { + xfree(di); + return; + } + lp->ll_tv = &di->di_tv; + } else { + if (watched) { + tv_copy(lp->ll_tv, &oldtv); + } + + if (op != NULL && *op != '=') { + eexe_mod_op(lp->ll_tv, rettv, op); + goto notify; + } else { + tv_clear(lp->ll_tv); + } + } + + // Assign the value to the variable or list item. + if (copy) { + tv_copy(rettv, lp->ll_tv); + } else { + *lp->ll_tv = *rettv; + lp->ll_tv->v_lock = VAR_UNLOCKED; + tv_init(rettv); + } + +notify: + if (watched) { + if (oldtv.v_type == VAR_UNKNOWN) { + assert(lp->ll_newkey != NULL); + tv_dict_watcher_notify(dict, lp->ll_newkey, lp->ll_tv, NULL); + } else { + dictitem_T *di_ = lp->ll_di; + assert(di_->di_key != NULL); + tv_dict_watcher_notify(dict, (char *)di_->di_key, lp->ll_tv, &oldtv); + tv_clear(&oldtv); + } + } + } +} + +// TODO(ZyX-I): move to eval/ex_cmds + +/// Evaluate the expression used in a ":for var in expr" command. +/// "arg" points to "var". +/// +/// @param[out] *errp set to TRUE for an error, FALSE otherwise; +/// +/// @return a pointer that holds the info. Null when there is an error. +void *eval_for_line(const char *arg, bool *errp, char **nextcmdp, int skip) +{ + forinfo_T *fi = xcalloc(1, sizeof(forinfo_T)); + const char *expr; + typval_T tv; + list_T *l; + + *errp = true; // Default: there is an error. + + expr = skip_var_list((char *)arg, &fi->fi_varcount, &fi->fi_semicolon); + if (expr == NULL) { + return fi; + } + + expr = skipwhite(expr); + if (expr[0] != 'i' || expr[1] != 'n' || !ascii_iswhite(expr[2])) { + emsg(_("E690: Missing \"in\" after :for")); + return fi; + } + + if (skip) { + ++emsg_skip; + } + if (eval0(skipwhite(expr + 2), &tv, nextcmdp, !skip) == OK) { + *errp = false; + if (!skip) { + if (tv.v_type == VAR_LIST) { + l = tv.vval.v_list; + if (l == NULL) { + // a null list is like an empty list: do nothing + tv_clear(&tv); + } else { + // No need to increment the refcount, it's already set for + // the list being used in "tv". + fi->fi_list = l; + tv_list_watch_add(l, &fi->fi_lw); + fi->fi_lw.lw_item = tv_list_first(l); + } + } else if (tv.v_type == VAR_BLOB) { + fi->fi_bi = 0; + if (tv.vval.v_blob != NULL) { + typval_T btv; + + // Make a copy, so that the iteration still works when the + // blob is changed. + tv_blob_copy(&tv, &btv); + fi->fi_blob = btv.vval.v_blob; + } + tv_clear(&tv); + } else if (tv.v_type == VAR_STRING) { + fi->fi_byte_idx = 0; + fi->fi_string = tv.vval.v_string; + tv.vval.v_string = NULL; + if (fi->fi_string == NULL) { + fi->fi_string = xstrdup(""); + } + } else { + emsg(_(e_string_list_or_blob_required)); + tv_clear(&tv); + } + } + } + if (skip) { + --emsg_skip; + } + + return fi; +} + +// TODO(ZyX-I): move to eval/ex_cmds + +/// Use the first item in a ":for" list. Advance to the next. +/// Assign the values to the variable (list). "arg" points to the first one. +/// +/// @return true when a valid item was found, false when at end of list or +/// something wrong. +bool next_for_item(void *fi_void, char *arg) +{ + forinfo_T *fi = (forinfo_T *)fi_void; + + if (fi->fi_blob != NULL) { + if (fi->fi_bi >= tv_blob_len(fi->fi_blob)) { + return false; + } + typval_T tv; + tv.v_type = VAR_NUMBER; + tv.v_lock = VAR_FIXED; + tv.vval.v_number = tv_blob_get(fi->fi_blob, fi->fi_bi); + fi->fi_bi++; + return ex_let_vars(arg, &tv, true, fi->fi_semicolon, fi->fi_varcount, false, NULL) == OK; + } + + if (fi->fi_string != NULL) { + const int len = utfc_ptr2len(fi->fi_string + fi->fi_byte_idx); + if (len == 0) { + return false; + } + typval_T tv; + tv.v_type = VAR_STRING; + tv.v_lock = VAR_FIXED; + tv.vval.v_string = xstrnsave(fi->fi_string + fi->fi_byte_idx, (size_t)len); + fi->fi_byte_idx += len; + const int result + = ex_let_vars(arg, &tv, true, fi->fi_semicolon, fi->fi_varcount, false, NULL) == OK; + xfree(tv.vval.v_string); + return result; + } + + listitem_T *item = fi->fi_lw.lw_item; + if (item == NULL) { + return false; + } else { + fi->fi_lw.lw_item = TV_LIST_ITEM_NEXT(fi->fi_list, item); + return (ex_let_vars(arg, TV_LIST_ITEM_TV(item), true, + fi->fi_semicolon, fi->fi_varcount, false, NULL) == OK); + } +} + +// TODO(ZyX-I): move to eval/ex_cmds + +/// Free the structure used to store info used by ":for". +void free_for_info(void *fi_void) +{ + forinfo_T *fi = (forinfo_T *)fi_void; + + if (fi == NULL) { + return; + } + if (fi->fi_list != NULL) { + tv_list_watch_remove(fi->fi_list, &fi->fi_lw); + tv_list_unref(fi->fi_list); + } else if (fi->fi_blob != NULL) { + tv_blob_unref(fi->fi_blob); + } else { + xfree(fi->fi_string); + } + xfree(fi); +} + +void set_context_for_expression(expand_T *xp, char *arg, cmdidx_T cmdidx) + FUNC_ATTR_NONNULL_ALL +{ + int got_eq = FALSE; + int c; + char *p; + + if (cmdidx == CMD_let || cmdidx == CMD_const) { + xp->xp_context = EXPAND_USER_VARS; + if (strpbrk(arg, "\"'+-*/%.=!?~|&$([<>,#") == NULL) { + // ":let var1 var2 ...": find last space. + for (p = arg + STRLEN(arg); p >= arg;) { + xp->xp_pattern = p; + MB_PTR_BACK(arg, p); + if (ascii_iswhite(*p)) { + break; + } + } + return; + } + } else { + xp->xp_context = cmdidx == CMD_call ? EXPAND_FUNCTIONS + : EXPAND_EXPRESSION; + } + while ((xp->xp_pattern = strpbrk(arg, "\"'+-*/%.=!?~|&$([<>,#")) != NULL) { + c = (uint8_t)(*xp->xp_pattern); + if (c == '&') { + c = (uint8_t)xp->xp_pattern[1]; + if (c == '&') { + ++xp->xp_pattern; + xp->xp_context = cmdidx != CMD_let || got_eq + ? EXPAND_EXPRESSION : EXPAND_NOTHING; + } else if (c != ' ') { + xp->xp_context = EXPAND_SETTINGS; + if ((c == 'l' || c == 'g') && xp->xp_pattern[2] == ':') { + xp->xp_pattern += 2; + } + } + } else if (c == '$') { + // environment variable + xp->xp_context = EXPAND_ENV_VARS; + } else if (c == '=') { + got_eq = TRUE; + xp->xp_context = EXPAND_EXPRESSION; + } else if (c == '#' + && xp->xp_context == EXPAND_EXPRESSION) { + // Autoload function/variable contains '#' + break; + } else if ((c == '<' || c == '#') + && xp->xp_context == EXPAND_FUNCTIONS + && vim_strchr(xp->xp_pattern, '(') == NULL) { + // Function name can start with "<SNR>" and contain '#'. + break; + } else if (cmdidx != CMD_let || got_eq) { + if (c == '"') { // string + while ((c = (uint8_t)(*++xp->xp_pattern)) != NUL && c != '"') { + if (c == '\\' && xp->xp_pattern[1] != NUL) { + xp->xp_pattern++; + } + } + xp->xp_context = EXPAND_NOTHING; + } else if (c == '\'') { // literal string + // Trick: '' is like stopping and starting a literal string. + while ((c = (uint8_t)(*++xp->xp_pattern)) != NUL && c != '\'') {} + xp->xp_context = EXPAND_NOTHING; + } else if (c == '|') { + if (xp->xp_pattern[1] == '|') { + ++xp->xp_pattern; + xp->xp_context = EXPAND_EXPRESSION; + } else { + xp->xp_context = EXPAND_COMMANDS; + } + } else { + xp->xp_context = EXPAND_EXPRESSION; + } + } else { + // Doesn't look like something valid, expand as an expression + // anyway. + xp->xp_context = EXPAND_EXPRESSION; + } + arg = xp->xp_pattern; + if (*arg != NUL) { + while ((c = (char_u)(*++arg)) != NUL && (c == ' ' || c == '\t')) {} + } + } + + // ":exe one two" completes "two" + if ((cmdidx == CMD_execute + || cmdidx == CMD_echo + || cmdidx == CMD_echon + || cmdidx == CMD_echomsg) + && xp->xp_context == EXPAND_EXPRESSION) { + for (;;) { + char *const n = (char *)skiptowhite((char_u *)arg); + + if (n == arg || ascii_iswhite_or_nul(*skipwhite(n))) { + break; + } + arg = skipwhite(n); + } + } + + xp->xp_pattern = arg; +} + +/// Delete all "menutrans_" variables. +void del_menutrans_vars(void) +{ + hash_lock(&globvarht); + HASHTAB_ITER(&globvarht, hi, { + if (STRNCMP(hi->hi_key, "menutrans_", 10) == 0) { + delete_var(&globvarht, hi); + } + }); + hash_unlock(&globvarht); +} + +/* + * Local string buffer for the next two functions to store a variable name + * with its prefix. Allocated in cat_prefix_varname(), freed later in + * get_user_var_name(). + */ + +static char *varnamebuf = NULL; +static size_t varnamebuflen = 0; + +/// Function to concatenate a prefix and a variable name. +char *cat_prefix_varname(int prefix, const char *name) + FUNC_ATTR_NONNULL_ALL +{ + size_t len = STRLEN(name) + 3; + + if (len > varnamebuflen) { + xfree(varnamebuf); + len += 10; // some additional space + varnamebuf = xmalloc(len); + varnamebuflen = len; + } + *varnamebuf = (char)prefix; + varnamebuf[1] = ':'; + STRCPY(varnamebuf + 2, name); + return varnamebuf; +} + +/// Function given to ExpandGeneric() to obtain the list of user defined +/// (global/buffer/window/built-in) variable names. +char *get_user_var_name(expand_T *xp, int idx) +{ + static size_t gdone; + static size_t bdone; + static size_t wdone; + static size_t tdone; + static size_t vidx; + static hashitem_T *hi; + + if (idx == 0) { + gdone = bdone = wdone = vidx = 0; + tdone = 0; + } + + // Global variables + if (gdone < globvarht.ht_used) { + if (gdone++ == 0) { + hi = globvarht.ht_array; + } else { + ++hi; + } + while (HASHITEM_EMPTY(hi)) { + ++hi; + } + if (STRNCMP("g:", xp->xp_pattern, 2) == 0) { + return cat_prefix_varname('g', (char *)hi->hi_key); + } + return (char *)hi->hi_key; + } + + // b: variables + const hashtab_T *ht = &prevwin_curwin()->w_buffer->b_vars->dv_hashtab; + if (bdone < ht->ht_used) { + if (bdone++ == 0) { + hi = ht->ht_array; + } else { + ++hi; + } + while (HASHITEM_EMPTY(hi)) { + ++hi; + } + return cat_prefix_varname('b', (char *)hi->hi_key); + } + + // w: variables + ht = &prevwin_curwin()->w_vars->dv_hashtab; + if (wdone < ht->ht_used) { + if (wdone++ == 0) { + hi = ht->ht_array; + } else { + ++hi; + } + while (HASHITEM_EMPTY(hi)) { + ++hi; + } + return cat_prefix_varname('w', (char *)hi->hi_key); + } + + // t: variables + ht = &curtab->tp_vars->dv_hashtab; + if (tdone < ht->ht_used) { + if (tdone++ == 0) { + hi = ht->ht_array; + } else { + ++hi; + } + while (HASHITEM_EMPTY(hi)) { + ++hi; + } + return cat_prefix_varname('t', (char *)hi->hi_key); + } + + // v: variables + if (vidx < ARRAY_SIZE(vimvars)) { + return cat_prefix_varname('v', vimvars[vidx++].vv_name); + } + + XFREE_CLEAR(varnamebuf); + varnamebuflen = 0; + return NULL; +} + +// TODO(ZyX-I): move to eval/expressions + +/// Does not use 'cpo' and always uses 'magic'. +/// +/// @return TRUE if "pat" matches "text". +int pattern_match(char *pat, char *text, bool ic) +{ + int matches = 0; + regmatch_T regmatch; + + // avoid 'l' flag in 'cpoptions' + char *save_cpo = p_cpo; + p_cpo = ""; + regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING); + if (regmatch.regprog != NULL) { + regmatch.rm_ic = ic; + matches = vim_regexec_nl(®match, (char_u *)text, (colnr_T)0); + vim_regfree(regmatch.regprog); + } + p_cpo = save_cpo; + return matches; +} + +/// Handle a name followed by "(". Both for just "name(arg)" and for +/// "expr->name(arg)". +/// +/// @param arg Points to "(", will be advanced +/// @param basetv "expr" for "expr->name(arg)" +/// +/// @return OK or FAIL. +static int eval_func(char **const arg, char *const name, const int name_len, typval_T *const rettv, + const bool evaluate, typval_T *const basetv) + FUNC_ATTR_NONNULL_ARG(1, 2, 4) +{ + char *s = name; + int len = name_len; + + if (!evaluate) { + check_vars((const char *)s, (size_t)len); + } + + // If "s" is the name of a variable of type VAR_FUNC + // use its contents. + partial_T *partial; + s = (char *)deref_func_name((const char *)s, &len, &partial, !evaluate); + + // Need to make a copy, in case evaluating the arguments makes + // the name invalid. + s = xmemdupz(s, (size_t)len); + + // Invoke the function. + funcexe_T funcexe = FUNCEXE_INIT; + funcexe.firstline = curwin->w_cursor.lnum; + funcexe.lastline = curwin->w_cursor.lnum; + funcexe.evaluate = evaluate; + funcexe.partial = partial; + funcexe.basetv = basetv; + int ret = get_func_tv((char_u *)s, len, rettv, (char_u **)arg, &funcexe); + + xfree(s); + + // If evaluate is false rettv->v_type was not set in + // get_func_tv, but it's needed in handle_subscript() to parse + // what follows. So set it here. + if (rettv->v_type == VAR_UNKNOWN && !evaluate && **arg == '(') { + rettv->vval.v_string = (char *)tv_empty_string; + rettv->v_type = VAR_FUNC; + } + + // Stop the expression evaluation when immediately + // aborting on error, or when an interrupt occurred or + // an exception was thrown but not caught. + if (evaluate && aborting()) { + if (ret == OK) { + tv_clear(rettv); + } + ret = FAIL; + } + return ret; +} + +// TODO(ZyX-I): move to eval/expressions + +/* + * The "evaluate" argument: When FALSE, the argument is only parsed but not + * executed. The function may return OK, but the rettv will be of type + * VAR_UNKNOWN. The function still returns FAIL for a syntax error. + */ + +/// Handle zero level expression. +/// This calls eval1() and handles error message and nextcmd. +/// Put the result in "rettv" when returning OK and "evaluate" is TRUE. +/// Note: "rettv.v_lock" is not set. +/// +/// @return OK or FAIL. +int eval0(char *arg, typval_T *rettv, char **nextcmd, int evaluate) +{ + int ret; + char *p; + const int did_emsg_before = did_emsg; + const int called_emsg_before = called_emsg; + + p = skipwhite(arg); + ret = eval1(&p, rettv, evaluate); + if (ret == FAIL || !ends_excmd(*p)) { + if (ret != FAIL) { + tv_clear(rettv); + } + // Report the invalid expression unless the expression evaluation has + // been cancelled due to an aborting error, an interrupt, or an + // exception, or we already gave a more specific error. + // Also check called_emsg for when using assert_fails(). + if (!aborting() && did_emsg == did_emsg_before + && called_emsg == called_emsg_before) { + semsg(_(e_invexpr2), arg); + } + ret = FAIL; + } + if (nextcmd != NULL) { + *nextcmd = (char *)check_nextcmd((char_u *)p); + } + + return ret; +} + +// TODO(ZyX-I): move to eval/expressions + +/// Handle top level expression: +/// expr2 ? expr1 : expr1 +/// +/// "arg" must point to the first non-white of the expression. +/// "arg" is advanced to the next non-white after the recognized expression. +/// +/// Note: "rettv.v_lock" is not set. +/// +/// @return OK or FAIL. +int eval1(char **arg, typval_T *rettv, int evaluate) +{ + int result; + typval_T var2; + + /* + * Get the first variable. + */ + if (eval2(arg, rettv, evaluate) == FAIL) { + return FAIL; + } + + if ((*arg)[0] == '?') { + result = FALSE; + if (evaluate) { + bool error = false; + + if (tv_get_number_chk(rettv, &error) != 0) { + result = true; + } + tv_clear(rettv); + if (error) { + return FAIL; + } + } + + /* + * Get the second variable. + */ + *arg = skipwhite(*arg + 1); + if (eval1(arg, rettv, evaluate && result) == FAIL) { // recursive! + return FAIL; + } + + /* + * Check for the ":". + */ + if ((*arg)[0] != ':') { + emsg(_("E109: Missing ':' after '?'")); + if (evaluate && result) { + tv_clear(rettv); + } + return FAIL; + } + + /* + * Get the third variable. + */ + *arg = skipwhite(*arg + 1); + if (eval1(arg, &var2, evaluate && !result) == FAIL) { // Recursive! + if (evaluate && result) { + tv_clear(rettv); + } + return FAIL; + } + if (evaluate && !result) { + *rettv = var2; + } + } + + return OK; +} + +// TODO(ZyX-I): move to eval/expressions + +/// Handle first level expression: +/// expr2 || expr2 || expr2 logical OR +/// +/// "arg" must point to the first non-white of the expression. +/// "arg" is advanced to the next non-white after the recognized expression. +/// +/// @return OK or FAIL. +static int eval2(char **arg, typval_T *rettv, int evaluate) +{ + typval_T var2; + long result; + int first; + bool error = false; + + /* + * Get the first variable. + */ + if (eval3(arg, rettv, evaluate) == FAIL) { + return FAIL; + } + + /* + * Repeat until there is no following "||". + */ + first = TRUE; + result = FALSE; + while ((*arg)[0] == '|' && (*arg)[1] == '|') { + if (evaluate && first) { + if (tv_get_number_chk(rettv, &error) != 0) { + result = true; + } + tv_clear(rettv); + if (error) { + return FAIL; + } + first = false; + } + + /* + * Get the second variable. + */ + *arg = skipwhite(*arg + 2); + if (eval3(arg, &var2, evaluate && !result) == FAIL) { + return FAIL; + } + + /* + * Compute the result. + */ + if (evaluate && !result) { + if (tv_get_number_chk(&var2, &error) != 0) { + result = true; + } + tv_clear(&var2); + if (error) { + return FAIL; + } + } + if (evaluate) { + rettv->v_type = VAR_NUMBER; + rettv->vval.v_number = result; + } + } + + return OK; +} + +// TODO(ZyX-I): move to eval/expressions + +/// Handle second level expression: +/// expr3 && expr3 && expr3 logical AND +/// +/// @param arg must point to the first non-white of the expression. +/// `arg` is advanced to the next non-white after the recognized expression. +/// +/// @return OK or FAIL. +static int eval3(char **arg, typval_T *rettv, int evaluate) +{ + typval_T var2; + long result; + int first; + bool error = false; + + /* + * Get the first variable. + */ + if (eval4(arg, rettv, evaluate) == FAIL) { + return FAIL; + } + + /* + * Repeat until there is no following "&&". + */ + first = TRUE; + result = TRUE; + while ((*arg)[0] == '&' && (*arg)[1] == '&') { + if (evaluate && first) { + if (tv_get_number_chk(rettv, &error) == 0) { + result = false; + } + tv_clear(rettv); + if (error) { + return FAIL; + } + first = false; + } + + /* + * Get the second variable. + */ + *arg = skipwhite(*arg + 2); + if (eval4(arg, &var2, evaluate && result) == FAIL) { + return FAIL; + } + + /* + * Compute the result. + */ + if (evaluate && result) { + if (tv_get_number_chk(&var2, &error) == 0) { + result = false; + } + tv_clear(&var2); + if (error) { + return FAIL; + } + } + if (evaluate) { + rettv->v_type = VAR_NUMBER; + rettv->vval.v_number = result; + } + } + + return OK; +} + +// TODO(ZyX-I): move to eval/expressions + +/// Handle third level expression: +/// var1 == var2 +/// var1 =~ var2 +/// var1 != var2 +/// var1 !~ var2 +/// var1 > var2 +/// var1 >= var2 +/// var1 < var2 +/// var1 <= var2 +/// var1 is var2 +/// var1 isnot var2 +/// +/// "arg" must point to the first non-white of the expression. +/// "arg" is advanced to the next non-white after the recognized expression. +/// +/// @return OK or FAIL. +static int eval4(char **arg, typval_T *rettv, int evaluate) +{ + typval_T var2; + char *p; + exprtype_T type = EXPR_UNKNOWN; + int len = 2; + bool ic; + + /* + * Get the first variable. + */ + if (eval5(arg, rettv, evaluate) == FAIL) { + return FAIL; + } + + p = *arg; + switch (p[0]) { + case '=': + if (p[1] == '=') { + type = EXPR_EQUAL; + } else if (p[1] == '~') { + type = EXPR_MATCH; + } + break; + case '!': + if (p[1] == '=') { + type = EXPR_NEQUAL; + } else if (p[1] == '~') { + type = EXPR_NOMATCH; + } + break; + case '>': + if (p[1] != '=') { + type = EXPR_GREATER; + len = 1; + } else { + type = EXPR_GEQUAL; + } + break; + case '<': + if (p[1] != '=') { + type = EXPR_SMALLER; + len = 1; + } else { + type = EXPR_SEQUAL; + } + break; + case 'i': + if (p[1] == 's') { + if (p[2] == 'n' && p[3] == 'o' && p[4] == 't') { + len = 5; + } + if (!isalnum(p[len]) && p[len] != '_') { + type = len == 2 ? EXPR_IS : EXPR_ISNOT; + } + } + break; + } + + /* + * If there is a comparative operator, use it. + */ + if (type != EXPR_UNKNOWN) { + // extra question mark appended: ignore case + if (p[len] == '?') { + ic = true; + len++; + } else if (p[len] == '#') { // extra '#' appended: match case + ic = false; + len++; + } else { // nothing appended: use 'ignorecase' + ic = p_ic; + } + + // Get the second variable. + *arg = skipwhite(p + len); + if (eval5(arg, &var2, evaluate) == FAIL) { + tv_clear(rettv); + return FAIL; + } + if (evaluate) { + const int ret = typval_compare(rettv, &var2, type, ic); + + tv_clear(&var2); + return ret; + } + } + + return OK; +} + +// TODO(ZyX-I): move to eval/expressions + +/// Handle fourth level expression: +/// + number addition +/// - number subtraction +/// . string concatenation +/// .. string concatenation +/// +/// @param arg must point to the first non-white of the expression. +/// `arg` is advanced to the next non-white after the recognized expression. +/// +/// @return OK or FAIL. +static int eval5(char **arg, typval_T *rettv, int evaluate) +{ + typval_T var2; + typval_T var3; + int op; + varnumber_T n1, n2; + float_T f1 = 0, f2 = 0; + char *p; + + /* + * Get the first variable. + */ + if (eval6(arg, rettv, evaluate, FALSE) == FAIL) { + return FAIL; + } + + /* + * Repeat computing, until no '+', '-' or '.' is following. + */ + for (;;) { + op = (char_u)(**arg); + if (op != '+' && op != '-' && op != '.') { + break; + } + + if ((op != '+' || (rettv->v_type != VAR_LIST && rettv->v_type != VAR_BLOB)) + && (op == '.' || rettv->v_type != VAR_FLOAT)) { + // For "list + ...", an illegal use of the first operand as + // a number cannot be determined before evaluating the 2nd + // operand: if this is also a list, all is ok. + // For "something . ...", "something - ..." or "non-list + ...", + // we know that the first operand needs to be a string or number + // without evaluating the 2nd operand. So check before to avoid + // side effects after an error. + if (evaluate && !tv_check_str(rettv)) { + tv_clear(rettv); + return FAIL; + } + } + + /* + * Get the second variable. + */ + if (op == '.' && *(*arg + 1) == '.') { // ..string concatenation + (*arg)++; + } + *arg = skipwhite(*arg + 1); + if (eval6(arg, &var2, evaluate, op == '.') == FAIL) { + tv_clear(rettv); + return FAIL; + } + + if (evaluate) { + /* + * Compute the result. + */ + if (op == '.') { + char buf1[NUMBUFLEN]; + char buf2[NUMBUFLEN]; + // s1 already checked + const char *const s1 = tv_get_string_buf(rettv, buf1); + const char *const s2 = tv_get_string_buf_chk(&var2, buf2); + if (s2 == NULL) { // Type error? + tv_clear(rettv); + tv_clear(&var2); + return FAIL; + } + p = (char *)concat_str((const char_u *)s1, (const char_u *)s2); + tv_clear(rettv); + rettv->v_type = VAR_STRING; + rettv->vval.v_string = p; + } else if (op == '+' && rettv->v_type == VAR_BLOB + && var2.v_type == VAR_BLOB) { + const blob_T *const b1 = rettv->vval.v_blob; + const blob_T *const b2 = var2.vval.v_blob; + blob_T *const b = tv_blob_alloc(); + + for (int i = 0; i < tv_blob_len(b1); i++) { + ga_append(&b->bv_ga, (char)tv_blob_get(b1, i)); + } + for (int i = 0; i < tv_blob_len(b2); i++) { + ga_append(&b->bv_ga, (char)tv_blob_get(b2, i)); + } + + tv_clear(rettv); + tv_blob_set_ret(rettv, b); + } else if (op == '+' && rettv->v_type == VAR_LIST + && var2.v_type == VAR_LIST) { + // Concatenate Lists. + if (tv_list_concat(rettv->vval.v_list, var2.vval.v_list, &var3) + == FAIL) { + tv_clear(rettv); + tv_clear(&var2); + return FAIL; + } + tv_clear(rettv); + *rettv = var3; + } else { + bool error = false; + + if (rettv->v_type == VAR_FLOAT) { + f1 = rettv->vval.v_float; + n1 = 0; + } else { + n1 = tv_get_number_chk(rettv, &error); + if (error) { + // This can only happen for "list + non-list" or + // "blob + non-blob". For "non-list + ..." or + // "something - ...", we returned before evaluating the + // 2nd operand. + tv_clear(rettv); + tv_clear(&var2); + return FAIL; + } + if (var2.v_type == VAR_FLOAT) { + f1 = (float_T)n1; + } + } + if (var2.v_type == VAR_FLOAT) { + f2 = var2.vval.v_float; + n2 = 0; + } else { + n2 = tv_get_number_chk(&var2, &error); + if (error) { + tv_clear(rettv); + tv_clear(&var2); + return FAIL; + } + if (rettv->v_type == VAR_FLOAT) { + f2 = (float_T)n2; + } + } + tv_clear(rettv); + + // If there is a float on either side the result is a float. + if (rettv->v_type == VAR_FLOAT || var2.v_type == VAR_FLOAT) { + if (op == '+') { + f1 = f1 + f2; + } else { + f1 = f1 - f2; + } + rettv->v_type = VAR_FLOAT; + rettv->vval.v_float = f1; + } else { + if (op == '+') { + n1 = n1 + n2; + } else { + n1 = n1 - n2; + } + rettv->v_type = VAR_NUMBER; + rettv->vval.v_number = n1; + } + } + tv_clear(&var2); + } + } + return OK; +} + +// TODO(ZyX-I): move to eval/expressions + +/// Handle fifth level expression: +/// - * number multiplication +/// - / number division +/// - % number modulo +/// +/// @param[in,out] arg Points to the first non-whitespace character of the +/// expression. Is advanced to the next non-whitespace +/// character after the recognized expression. +/// @param[out] rettv Location where result is saved. +/// @param[in] evaluate If not true, rettv is not populated. +/// @param[in] want_string True if "." is string_concatenation, otherwise +/// float +/// @return OK or FAIL. +static int eval6(char **arg, typval_T *rettv, int evaluate, int want_string) + FUNC_ATTR_NO_SANITIZE_UNDEFINED +{ + typval_T var2; + int op; + varnumber_T n1, n2; + bool use_float = false; + float_T f1 = 0, f2 = 0; + bool error = false; + + /* + * Get the first variable. + */ + if (eval7(arg, rettv, evaluate, want_string) == FAIL) { + return FAIL; + } + + /* + * Repeat computing, until no '*', '/' or '%' is following. + */ + for (;;) { + op = (char_u)(**arg); + if (op != '*' && op != '/' && op != '%') { + break; + } + + if (evaluate) { + if (rettv->v_type == VAR_FLOAT) { + f1 = rettv->vval.v_float; + use_float = true; + n1 = 0; + } else { + n1 = tv_get_number_chk(rettv, &error); + } + tv_clear(rettv); + if (error) { + return FAIL; + } + } else { + n1 = 0; + } + + /* + * Get the second variable. + */ + *arg = skipwhite(*arg + 1); + if (eval7(arg, &var2, evaluate, false) == FAIL) { + return FAIL; + } + + if (evaluate) { + if (var2.v_type == VAR_FLOAT) { + if (!use_float) { + f1 = (float_T)n1; + use_float = true; + } + f2 = var2.vval.v_float; + n2 = 0; + } else { + n2 = tv_get_number_chk(&var2, &error); + tv_clear(&var2); + if (error) { + return FAIL; + } + if (use_float) { + f2 = (float_T)n2; + } + } + + /* + * Compute the result. + * When either side is a float the result is a float. + */ + if (use_float) { + if (op == '*') { + f1 = f1 * f2; + } else if (op == '/') { + // uncrustify:off + + // Division by zero triggers error from AddressSanitizer + f1 = (f2 == 0 ? ( +#ifdef NAN + f1 == 0 ? (float_T)NAN : +#endif + (f1 > 0 ? (float_T)INFINITY : (float_T)-INFINITY)) : f1 / f2); + + // uncrustify:on + } else { + emsg(_("E804: Cannot use '%' with Float")); + return FAIL; + } + rettv->v_type = VAR_FLOAT; + rettv->vval.v_float = f1; + } else { + if (op == '*') { + n1 = n1 * n2; + } else if (op == '/') { + n1 = num_divide(n1, n2); + } else { + n1 = num_modulus(n1, n2); + } + rettv->v_type = VAR_NUMBER; + rettv->vval.v_number = n1; + } + } + } + + return OK; +} + +// TODO(ZyX-I): move to eval/expressions + +/// Handle sixth level expression: +/// number number constant +/// 0zFFFFFFFF Blob constant +/// "string" string constant +/// 'string' literal string constant +/// &option-name option value +/// @r register contents +/// identifier variable value +/// function() function call +/// $VAR environment variable +/// (expression) nested expression +/// [expr, expr] List +/// {key: val, key: val} Dictionary +/// #{key: val, key: val} Dictionary with literal keys +/// +/// Also handle: +/// ! in front logical NOT +/// - in front unary minus +/// + in front unary plus (ignored) +/// trailing [] subscript in String or List +/// trailing .name entry in Dictionary +/// trailing ->name() method call +/// +/// "arg" must point to the first non-white of the expression. +/// "arg" is advanced to the next non-white after the recognized expression. +/// +/// @param want_string after "." operator +/// +/// @return OK or FAIL. +static int eval7(char **arg, typval_T *rettv, int evaluate, int want_string) +{ + varnumber_T n; + int len; + char *s; + const char *start_leader, *end_leader; + int ret = OK; + char *alias; + + // Initialise variable so that tv_clear() can't mistake this for a + // string and free a string that isn't there. + rettv->v_type = VAR_UNKNOWN; + + // Skip '!', '-' and '+' characters. They are handled later. + start_leader = *arg; + while (**arg == '!' || **arg == '-' || **arg == '+') { + *arg = skipwhite(*arg + 1); + } + end_leader = *arg; + + switch (**arg) { + // Number constant. + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': { + char *p = skipdigits(*arg + 1); + int get_float = false; + + // We accept a float when the format matches + // "[0-9]\+\.[0-9]\+\([eE][+-]\?[0-9]\+\)\?". This is very + // strict to avoid backwards compatibility problems. + // Don't look for a float after the "." operator, so that + // ":let vers = 1.2.3" doesn't fail. + if (!want_string && p[0] == '.' && ascii_isdigit(p[1])) { + get_float = true; + p = skipdigits(p + 2); + if (*p == 'e' || *p == 'E') { + ++p; + if (*p == '-' || *p == '+') { + ++p; + } + if (!ascii_isdigit(*p)) { + get_float = false; + } else { + p = skipdigits(p + 1); + } + } + if (ASCII_ISALPHA(*p) || *p == '.') { + get_float = false; + } + } + if (get_float) { + float_T f; + + *arg += string2float(*arg, &f); + if (evaluate) { + rettv->v_type = VAR_FLOAT; + rettv->vval.v_float = f; + } + } else if (**arg == '0' && ((*arg)[1] == 'z' || (*arg)[1] == 'Z')) { + blob_T *blob = NULL; + // Blob constant: 0z0123456789abcdef + if (evaluate) { + blob = tv_blob_alloc(); + } + char *bp; + for (bp = *arg + 2; ascii_isxdigit(bp[0]); bp += 2) { + if (!ascii_isxdigit(bp[1])) { + if (blob != NULL) { + emsg(_("E973: Blob literal should have an even number of hex " + "characters")); + ga_clear(&blob->bv_ga); + XFREE_CLEAR(blob); + } + ret = FAIL; + break; + } + if (blob != NULL) { + ga_append(&blob->bv_ga, (char)((hex2nr(*bp) << 4) + hex2nr(*(bp + 1)))); + } + if (bp[2] == '.' && ascii_isxdigit(bp[3])) { + bp++; + } + } + if (blob != NULL) { + tv_blob_set_ret(rettv, blob); + } + *arg = bp; + } else { + // decimal, hex or octal number + vim_str2nr((char_u *)(*arg), NULL, &len, STR2NR_ALL, &n, NULL, 0, true); + if (len == 0) { + semsg(_(e_invexpr2), *arg); + ret = FAIL; + break; + } + *arg += len; + if (evaluate) { + rettv->v_type = VAR_NUMBER; + rettv->vval.v_number = n; + } + } + break; + } + + // String constant: "string". + case '"': + ret = get_string_tv(arg, rettv, evaluate); + break; + + // Literal string constant: 'str''ing'. + case '\'': + ret = get_lit_string_tv(arg, rettv, evaluate); + break; + + // List: [expr, expr] + case '[': + ret = get_list_tv(arg, rettv, evaluate); + break; + + // Dictionary: #{key: val, key: val} + case '#': + if ((*arg)[1] == '{') { + (*arg)++; + ret = dict_get_tv(arg, rettv, evaluate, true); + } else { + ret = NOTDONE; + } + break; + + // Lambda: {arg, arg -> expr} + // Dictionary: {'key': val, 'key': val} + case '{': + ret = get_lambda_tv((char_u **)arg, rettv, evaluate); + if (ret == NOTDONE) { + ret = dict_get_tv(arg, rettv, evaluate, false); + } + break; + + // Option value: &name + case '&': + ret = get_option_tv((const char **)arg, rettv, evaluate); + break; + // Environment variable: $VAR. + case '$': + ret = get_env_tv(arg, rettv, evaluate); + break; + + // Register contents: @r. + case '@': + ++*arg; + int regname = mb_cptr2char_adv((const char_u**) arg); + if (evaluate) { + rettv->v_type = VAR_STRING; + rettv->vval.v_string = get_reg_contents(regname, kGRegExprSrc); + } + // if (**arg != NUL) { + // ++*arg; + // } + break; + + // nested expression: (expression). + case '(': + *arg = skipwhite(*arg + 1); + ret = eval1(arg, rettv, evaluate); // recursive! + if (**arg == ')') { + ++*arg; + } else if (ret == OK) { + emsg(_("E110: Missing ')'")); + tv_clear(rettv); + ret = FAIL; + } + break; + + default: + ret = NOTDONE; + break; + } + + if (ret == NOTDONE) { + // Must be a variable or function name. + // Can also be a curly-braces kind of name: {expr}. + s = *arg; + len = get_name_len((const char **)arg, &alias, evaluate, true); + if (alias != NULL) { + s = alias; + } + + if (len <= 0) { + ret = FAIL; + } else { + if (**arg == '(') { // recursive! + ret = eval_func(arg, s, len, rettv, evaluate, NULL); + } else if (evaluate) { + ret = get_var_tv((const char *)s, len, rettv, NULL, true, false); + } else { + check_vars((const char *)s, (size_t)len); + ret = OK; + } + } + xfree(alias); + } + + *arg = skipwhite(*arg); + + // Handle following '[', '(' and '.' for expr[expr], expr.name, + // expr(expr), expr->name(expr) + if (ret == OK) { + ret = handle_subscript((const char **)arg, rettv, evaluate, true, + (char *)start_leader, &end_leader); + } + + // Apply logical NOT and unary '-', from right to left, ignore '+'. + if (ret == OK && evaluate && end_leader > start_leader) { + ret = eval7_leader(rettv, (char *)start_leader, &end_leader); + } + return ret; +} + +/// Apply the leading "!" and "-" before an eval7 expression to "rettv". +/// Adjusts "end_leaderp" until it is at "start_leader". +/// +/// @return OK on success, FAIL on failure. +static int eval7_leader(typval_T *const rettv, const char *const start_leader, + const char **const end_leaderp) + FUNC_ATTR_NONNULL_ALL +{ + const char *end_leader = (char *)(*end_leaderp); + int ret = OK; + bool error = false; + varnumber_T val = 0; + float_T f = 0.0; + + if (rettv->v_type == VAR_FLOAT) { + f = rettv->vval.v_float; + } else { + val = tv_get_number_chk(rettv, &error); + } + if (error) { + tv_clear(rettv); + ret = FAIL; + } else { + while (end_leader > start_leader) { + end_leader--; + if (*end_leader == '!') { + if (rettv->v_type == VAR_FLOAT) { + f = !(bool)f; + } else { + val = !val; + } + } else if (*end_leader == '-') { + if (rettv->v_type == VAR_FLOAT) { + f = -f; + } else { + val = -val; + } + } + } + if (rettv->v_type == VAR_FLOAT) { + tv_clear(rettv); + rettv->vval.v_float = f; + } else { + tv_clear(rettv); + rettv->v_type = VAR_NUMBER; + rettv->vval.v_number = val; + } + } + + *end_leaderp = end_leader; + return ret; +} + +/// Call the function referred to in "rettv". +/// @param lua_funcname If `rettv` refers to a v:lua function, this must point +/// to the name of the Lua function to call (after the +/// "v:lua." prefix). +/// @return OK on success, FAIL on failure. +static int call_func_rettv(char **const arg, typval_T *const rettv, const bool evaluate, + dict_T *const selfdict, typval_T *const basetv, + const char *const lua_funcname) + FUNC_ATTR_NONNULL_ARG(1, 2) +{ + partial_T *pt = NULL; + typval_T functv; + const char *funcname; + bool is_lua = false; + + // need to copy the funcref so that we can clear rettv + if (evaluate) { + functv = *rettv; + rettv->v_type = VAR_UNKNOWN; + + // Invoke the function. Recursive! + if (functv.v_type == VAR_PARTIAL) { + pt = functv.vval.v_partial; + is_lua = is_luafunc(pt); + funcname = is_lua ? lua_funcname : partial_name(pt); + } else { + funcname = functv.vval.v_string; + } + } else { + funcname = ""; + } + + funcexe_T funcexe = FUNCEXE_INIT; + funcexe.firstline = curwin->w_cursor.lnum; + funcexe.lastline = curwin->w_cursor.lnum; + funcexe.evaluate = evaluate; + funcexe.partial = pt; + funcexe.selfdict = selfdict; + funcexe.basetv = basetv; + const int ret = get_func_tv((char_u *)funcname, is_lua ? (int)(*arg - funcname) : -1, rettv, + (char_u **)arg, &funcexe); + + // Clear the funcref afterwards, so that deleting it while + // evaluating the arguments is possible (see test55). + if (evaluate) { + tv_clear(&functv); + } + + return ret; +} + +/// Evaluate "->method()". +/// +/// @param verbose if true, give error messages. +/// @param *arg points to the '-'. +/// +/// @return FAIL or OK. +/// +/// @note "*arg" is advanced to after the ')'. +static int eval_lambda(char **const arg, typval_T *const rettv, const bool evaluate, + const bool verbose) + FUNC_ATTR_NONNULL_ALL +{ + // Skip over the ->. + *arg += 2; + typval_T base = *rettv; + rettv->v_type = VAR_UNKNOWN; + + int ret = get_lambda_tv((char_u **)arg, rettv, evaluate); + if (ret != OK) { + return FAIL; + } else if (**arg != '(') { + if (verbose) { + if (*skipwhite(*arg) == '(') { + emsg(_(e_nowhitespace)); + } else { + semsg(_(e_missingparen), "lambda"); + } + } + tv_clear(rettv); + ret = FAIL; + } else { + ret = call_func_rettv(arg, rettv, evaluate, NULL, &base, NULL); + } + + // Clear the funcref afterwards, so that deleting it while + // evaluating the arguments is possible (see test55). + if (evaluate) { + tv_clear(&base); + } + + return ret; +} + +/// Evaluate "->method()" or "->v:lua.method()". +/// +/// @param *arg points to the '-'. +/// +/// @return FAIL or OK. "*arg" is advanced to after the ')'. +static int eval_method(char **const arg, typval_T *const rettv, const bool evaluate, + const bool verbose) + FUNC_ATTR_NONNULL_ALL +{ + // Skip over the ->. + *arg += 2; + typval_T base = *rettv; + rettv->v_type = VAR_UNKNOWN; + + // Locate the method name. + int len; + char *name = *arg; + char *lua_funcname = NULL; + if (STRNCMP(name, "v:lua.", 6) == 0) { + lua_funcname = name + 6; + *arg = (char *)skip_luafunc_name((const char *)lua_funcname); + *arg = skipwhite(*arg); // to detect trailing whitespace later + len = (int)(*arg - lua_funcname); + } else { + char *alias; + len = get_name_len((const char **)arg, &alias, evaluate, true); + if (alias != NULL) { + name = alias; + } + } + + int ret; + if (len <= 0) { + if (verbose) { + if (lua_funcname == NULL) { + emsg(_("E260: Missing name after ->")); + } else { + semsg(_(e_invexpr2), name); + } + } + ret = FAIL; + } else { + if (**arg != '(') { + if (verbose) { + semsg(_(e_missingparen), name); + } + ret = FAIL; + } else if (ascii_iswhite((*arg)[-1])) { + if (verbose) { + emsg(_(e_nowhitespace)); + } + ret = FAIL; + } else if (lua_funcname != NULL) { + if (evaluate) { + rettv->v_type = VAR_PARTIAL; + rettv->vval.v_partial = vvlua_partial; + rettv->vval.v_partial->pt_refcount++; + } + ret = call_func_rettv(arg, rettv, evaluate, NULL, &base, lua_funcname); + } else { + ret = eval_func(arg, name, len, rettv, evaluate, &base); + } + } + + // Clear the funcref afterwards, so that deleting it while + // evaluating the arguments is possible (see test55). + if (evaluate) { + tv_clear(&base); + } + + return ret; +} + +// TODO(ZyX-I): move to eval/expressions + +/// Evaluate an "[expr]" or "[expr:expr]" index. Also "dict.key". +/// "*arg" points to the '[' or '.'. +/// +/// @param verbose give error messages +/// +/// @returns FAIL or OK. "*arg" is advanced to after the ']'. +static int eval_index(char **arg, typval_T *rettv, int evaluate, int verbose) +{ + bool empty1 = false; + bool empty2 = false; + long n1, n2 = 0; + ptrdiff_t len = -1; + int range = false; + char *key = NULL; + + switch (rettv->v_type) { + case VAR_FUNC: + case VAR_PARTIAL: + if (verbose) { + emsg(_("E695: Cannot index a Funcref")); + } + return FAIL; + case VAR_FLOAT: + if (verbose) { + emsg(_(e_float_as_string)); + } + return FAIL; + case VAR_BOOL: + case VAR_SPECIAL: + if (verbose) { + emsg(_("E909: Cannot index a special variable")); + } + return FAIL; + case VAR_UNKNOWN: + if (evaluate) { + return FAIL; + } + FALLTHROUGH; + case VAR_STRING: + case VAR_NUMBER: + case VAR_LIST: + case VAR_DICT: + case VAR_BLOB: + break; + } + + typval_T var1 = TV_INITIAL_VALUE; + typval_T var2 = TV_INITIAL_VALUE; + if (**arg == '.') { + /* + * dict.name + */ + key = *arg + 1; + for (len = 0; ASCII_ISALNUM(key[len]) || key[len] == '_'; len++) {} + if (len == 0) { + return FAIL; + } + *arg = skipwhite(key + len); + } else { + /* + * something[idx] + * + * Get the (first) variable from inside the []. + */ + *arg = skipwhite(*arg + 1); + if (**arg == ':') { + empty1 = true; + } else if (eval1(arg, &var1, evaluate) == FAIL) { // Recursive! + return FAIL; + } else if (evaluate && !tv_check_str(&var1)) { + // Not a number or string. + tv_clear(&var1); + return FAIL; + } + + /* + * Get the second variable from inside the [:]. + */ + if (**arg == ':') { + range = true; + *arg = skipwhite(*arg + 1); + if (**arg == ']') { + empty2 = true; + } else if (eval1(arg, &var2, evaluate) == FAIL) { // Recursive! + if (!empty1) { + tv_clear(&var1); + } + return FAIL; + } else if (evaluate && !tv_check_str(&var2)) { + // Not a number or string. + if (!empty1) { + tv_clear(&var1); + } + tv_clear(&var2); + return FAIL; + } + } + + // Check for the ']'. + if (**arg != ']') { + if (verbose) { + emsg(_(e_missbrac)); + } + tv_clear(&var1); + if (range) { + tv_clear(&var2); + } + return FAIL; + } + *arg = skipwhite(*arg + 1); // skip the ']' + } + + if (evaluate) { + n1 = 0; + if (!empty1 && rettv->v_type != VAR_DICT && !tv_is_luafunc(rettv)) { + n1 = tv_get_number(&var1); + tv_clear(&var1); + } + if (range) { + if (empty2) { + n2 = -1; + } else { + n2 = tv_get_number(&var2); + tv_clear(&var2); + } + } + + switch (rettv->v_type) { + case VAR_NUMBER: + case VAR_STRING: { + const char *const s = tv_get_string(rettv); + char *v; + len = (ptrdiff_t)strlen(s); + if (range) { + // The resulting variable is a substring. If the indexes + // are out of range the result is empty. + if (n1 < 0) { + n1 = len + n1; + if (n1 < 0) { + n1 = 0; + } + } + if (n2 < 0) { + n2 = len + n2; + } else if (n2 >= len) { + n2 = len; + } + if (n1 >= len || n2 < 0 || n1 > n2) { + v = NULL; + } else { + v = xmemdupz(s + n1, (size_t)(n2 - n1 + 1)); + } + } else { + // The resulting variable is a string of a single + // character. If the index is too big or negative the + // result is empty. + if (n1 >= len || n1 < 0) { + v = NULL; + } else { + v = xmemdupz(s + n1, 1); + } + } + tv_clear(rettv); + rettv->v_type = VAR_STRING; + rettv->vval.v_string = v; + break; + } + case VAR_BLOB: + len = tv_blob_len(rettv->vval.v_blob); + if (range) { + // The resulting variable is a sub-blob. If the indexes + // are out of range the result is empty. + if (n1 < 0) { + n1 = len + n1; + if (n1 < 0) { + n1 = 0; + } + } + if (n2 < 0) { + n2 = len + n2; + } else if (n2 >= len) { + n2 = len - 1; + } + if (n1 >= len || n2 < 0 || n1 > n2) { + tv_clear(rettv); + rettv->v_type = VAR_BLOB; + rettv->vval.v_blob = NULL; + } else { + blob_T *const blob = tv_blob_alloc(); + ga_grow(&blob->bv_ga, (int)(n2 - n1 + 1)); + blob->bv_ga.ga_len = (int)(n2 - n1 + 1); + for (long i = n1; i <= n2; i++) { + tv_blob_set(blob, (int)(i - n1), tv_blob_get(rettv->vval.v_blob, (int)i)); + } + tv_clear(rettv); + tv_blob_set_ret(rettv, blob); + } + } else { + // The resulting variable is a byte value. + // If the index is too big or negative that is an error. + if (n1 < 0) { + n1 = len + n1; + } + if (n1 < len && n1 >= 0) { + const int v = (int)tv_blob_get(rettv->vval.v_blob, (int)n1); + tv_clear(rettv); + rettv->v_type = VAR_NUMBER; + rettv->vval.v_number = v; + } else { + semsg(_(e_blobidx), (int64_t)n1); + } + } + break; + case VAR_LIST: + len = tv_list_len(rettv->vval.v_list); + if (n1 < 0) { + n1 = len + n1; + } + if (!empty1 && (n1 < 0 || n1 >= len)) { + // For a range we allow invalid values and return an empty + // list. A list index out of range is an error. + if (!range) { + if (verbose) { + semsg(_(e_listidx), (int64_t)n1); + } + return FAIL; + } + n1 = len; + } + if (range) { + list_T *l; + listitem_T *item; + + if (n2 < 0) { + n2 = len + n2; + } else if (n2 >= len) { + n2 = len - 1; + } + if (!empty2 && (n2 < 0 || n2 + 1 < n1)) { + n2 = -1; + } + l = tv_list_alloc(n2 - n1 + 1); + item = tv_list_find(rettv->vval.v_list, (int)n1); + while (n1++ <= n2) { + tv_list_append_tv(l, TV_LIST_ITEM_TV(item)); + item = TV_LIST_ITEM_NEXT(rettv->vval.v_list, item); + } + tv_clear(rettv); + tv_list_set_ret(rettv, l); + } else { + tv_copy(TV_LIST_ITEM_TV(tv_list_find(rettv->vval.v_list, (int)n1)), &var1); + tv_clear(rettv); + *rettv = var1; + } + break; + case VAR_DICT: { + if (range) { + if (verbose) { + emsg(_(e_dictrange)); + } + if (len == -1) { + tv_clear(&var1); + } + return FAIL; + } + + if (len == -1) { + key = (char *)tv_get_string_chk(&var1); + if (key == NULL) { + tv_clear(&var1); + return FAIL; + } + } + + dictitem_T *const item = tv_dict_find(rettv->vval.v_dict, + (const char *)key, len); + + if (item == NULL && verbose) { + semsg(_(e_dictkey), key); + } + if (len == -1) { + tv_clear(&var1); + } + if (item == NULL || tv_is_luafunc(&item->di_tv)) { + return FAIL; + } + + tv_copy(&item->di_tv, &var1); + tv_clear(rettv); + *rettv = var1; + break; + } + case VAR_BOOL: + case VAR_SPECIAL: + case VAR_FUNC: + case VAR_FLOAT: + case VAR_PARTIAL: + case VAR_UNKNOWN: + break; // Not evaluating, skipping over subscript + } + } + + return OK; +} + +// TODO(ZyX-I): move to eval/executor + +/// Get an option value +/// +/// @param[in,out] arg Points to the '&' or '+' before the option name. Is +/// advanced to the character after the option name. +/// @param[out] rettv Location where result is saved. +/// @param[in] evaluate If not true, rettv is not populated. +/// +/// @return OK or FAIL. +int get_option_tv(const char **const arg, typval_T *const rettv, const bool evaluate) + FUNC_ATTR_NONNULL_ARG(1) +{ + long numval; + char *stringval; + getoption_T opt_type; + bool working = (**arg == '+'); // has("+option") + int ret = OK; + int opt_flags; + + // Isolate the option name and find its value. + char *option_end = (char *)find_option_end(arg, &opt_flags); + if (option_end == NULL) { + if (rettv != NULL) { + semsg(_("E112: Option name missing: %s"), *arg); + } + return FAIL; + } + + if (!evaluate) { + *arg = option_end; + return OK; + } + + char c = *option_end; + *option_end = NUL; + opt_type = get_option_value(*arg, &numval, + rettv == NULL ? NULL : &stringval, opt_flags); + + if (opt_type == gov_unknown) { + if (rettv != NULL) { + semsg(_("E113: Unknown option: %s"), *arg); + } + ret = FAIL; + } else if (rettv != NULL) { + if (opt_type == gov_hidden_string) { + rettv->v_type = VAR_STRING; + rettv->vval.v_string = NULL; + } else if (opt_type == gov_hidden_bool || opt_type == gov_hidden_number) { + rettv->v_type = VAR_NUMBER; + rettv->vval.v_number = 0; + } else if (opt_type == gov_bool || opt_type == gov_number) { + rettv->v_type = VAR_NUMBER; + rettv->vval.v_number = numval; + } else { // string option + rettv->v_type = VAR_STRING; + rettv->vval.v_string = stringval; + } + } else if (working && (opt_type == gov_hidden_bool + || opt_type == gov_hidden_number + || opt_type == gov_hidden_string)) { + ret = FAIL; + } + + *option_end = c; // put back for error messages + *arg = option_end; + + return ret; +} + +/// Allocate a variable for a string constant. +/// +/// @return OK or FAIL. +static int get_string_tv(char **arg, typval_T *rettv, int evaluate) +{ + char *p; + unsigned int extra = 0; + + /* + * Find the end of the string, skipping backslashed characters. + */ + for (p = *arg + 1; *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 == '<') { + extra += 5; + } + } + } + + if (*p != '"') { + semsg(_("E114: Missing quote: %s"), *arg); + return FAIL; + } + + // If only parsing, set *arg and return here + if (!evaluate) { + *arg = p + 1; + return OK; + } + + /* + * Copy the string into allocated memory, handling backslashed + * characters. + */ + const int len = (int)(p - *arg + extra); + char *name = xmalloc((size_t)len); + rettv->v_type = VAR_STRING; + rettv->vval.v_string = name; + + for (p = *arg + 1; *p != NUL && *p != '"';) { + if (*p == '\\') { + switch (*++p) { + case 'b': + *name++ = BS; ++p; break; + case 'e': + *name++ = ESC; ++p; break; + case 'f': + *name++ = FF; ++p; break; + case 'n': + *name++ = NL; ++p; break; + case 'r': + *name++ = CAR; ++p; break; + case 't': + *name++ = TAB; ++p; break; + + case 'X': // hex: "\x1", "\x12" + case 'x': + case 'u': // Unicode: "\u0023" + case 'U': + if (ascii_isxdigit(p[1])) { + int n, nr; + int c = toupper(*p); + + if (c == 'X') { + n = 2; + } else if (*p == 'u') { + n = 4; + } else { + n = 8; + } + nr = 0; + while (--n >= 0 && ascii_isxdigit(p[1])) { + ++p; + nr = (nr << 4) + hex2nr(*p); + } + p++; + // For "\u" store the number according to + // 'encoding'. + if (c != 'X') { + name += utf_char2bytes(nr, name); + } else { + *name++ = (char)nr; + } + } + break; + + // octal: "\1", "\12", "\123" + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + *name = (char)(*p++ - '0'); + if (*p >= '0' && *p <= '7') { + *name = (char)((*name << 3) + *p++ - '0'); + if (*p >= '0' && *p <= '7') { + *name = (char)((*name << 3) + *p++ - '0'); + } + } + ++name; + break; + + // Special key, e.g.: "\<C-W>" + case '<': { + int flags = FSK_KEYCODE | FSK_IN_STRING; + + if (p[1] != '*') { + flags |= FSK_SIMPLIFY; + } + extra = trans_special((const char_u **)&p, STRLEN(p), (char_u *)name, flags, false, NULL); + if (extra != 0) { + name += extra; + if (name >= rettv->vval.v_string + len) { + iemsg("get_string_tv() used more space than allocated"); + } + break; + } + } + FALLTHROUGH; + + default: + mb_copy_char((const char_u **)&p, (char_u **)&name); + break; + } + } else { + mb_copy_char((const char_u **)&p, (char_u **)&name); + } + } + *name = NUL; + if (*p != NUL) { // just in case + p++; + } + *arg = p; + + return OK; +} + +/// Allocate a variable for a 'str''ing' constant. +/// +/// @return OK or FAIL. +static int get_lit_string_tv(char **arg, typval_T *rettv, int evaluate) +{ + char *p; + char *str; + int reduce = 0; + + /* + * Find the end of the string, skipping ''. + */ + for (p = *arg + 1; *p != NUL; MB_PTR_ADV(p)) { + if (*p == '\'') { + if (p[1] != '\'') { + break; + } + ++reduce; + ++p; + } + } + + if (*p != '\'') { + semsg(_("E115: Missing quote: %s"), *arg); + return FAIL; + } + + // If only parsing return after setting "*arg" + if (!evaluate) { + *arg = p + 1; + return OK; + } + + /* + * Copy the string into allocated memory, handling '' to ' reduction. + */ + str = xmalloc((size_t)((p - *arg) - reduce)); + rettv->v_type = VAR_STRING; + rettv->vval.v_string = str; + + for (p = *arg + 1; *p != NUL;) { + if (*p == '\'') { + if (p[1] != '\'') { + break; + } + ++p; + } + mb_copy_char((const char_u **)&p, (char_u **)&str); + } + *str = NUL; + *arg = p + 1; + + return OK; +} + +/// @return the function name of the partial. +char *partial_name(partial_T *pt) + FUNC_ATTR_PURE +{ + if (pt->pt_name != NULL) { + return (char *)pt->pt_name; + } + return (char *)pt->pt_func->uf_name; +} + +// TODO(ZyX-I): Move to eval/typval.h + +static void partial_free(partial_T *pt) +{ + for (int i = 0; i < pt->pt_argc; i++) { + tv_clear(&pt->pt_argv[i]); + } + xfree(pt->pt_argv); + tv_dict_unref(pt->pt_dict); + if (pt->pt_name != NULL) { + func_unref(pt->pt_name); + xfree(pt->pt_name); + } else { + func_ptr_unref(pt->pt_func); + } + xfree(pt); +} + +// TODO(ZyX-I): Move to eval/typval.h + +/// Unreference a closure: decrement the reference count and free it when it +/// becomes zero. +void partial_unref(partial_T *pt) +{ + if (pt != NULL && --pt->pt_refcount <= 0) { + partial_free(pt); + } +} + +/// Allocate a variable for a List and fill it from "*arg". +/// +/// @return OK or FAIL. +static int get_list_tv(char **arg, typval_T *rettv, int evaluate) +{ + list_T *l = NULL; + + if (evaluate) { + l = tv_list_alloc(kListLenShouldKnow); + } + + *arg = skipwhite(*arg + 1); + while (**arg != ']' && **arg != NUL) { + typval_T tv; + if (eval1(arg, &tv, evaluate) == FAIL) { // Recursive! + goto failret; + } + if (evaluate) { + tv.v_lock = VAR_UNLOCKED; + tv_list_append_owned_tv(l, tv); + } + + if (**arg == ']') { + break; + } + if (**arg != ',') { + semsg(_("E696: Missing comma in List: %s"), *arg); + goto failret; + } + *arg = skipwhite(*arg + 1); + } + + if (**arg != ']') { + semsg(_("E697: Missing end of List ']': %s"), *arg); +failret: + if (evaluate) { + tv_list_free(l); + } + return FAIL; + } + + *arg = skipwhite(*arg + 1); + if (evaluate) { + tv_list_set_ret(rettv, l); + } + + return OK; +} + +/// @param ic ignore case +bool func_equal(typval_T *tv1, typval_T *tv2, bool ic) +{ + char_u *s1, *s2; + dict_T *d1, *d2; + int a1, a2; + + // empty and NULL function name considered the same + s1 = (char_u *)(tv1->v_type == VAR_FUNC ? tv1->vval.v_string : partial_name(tv1->vval.v_partial)); + if (s1 != NULL && *s1 == NUL) { + s1 = NULL; + } + s2 = (char_u *)(tv2->v_type == VAR_FUNC ? tv2->vval.v_string : partial_name(tv2->vval.v_partial)); + if (s2 != NULL && *s2 == NUL) { + s2 = NULL; + } + if (s1 == NULL || s2 == NULL) { + if (s1 != s2) { + return false; + } + } else if (STRCMP(s1, s2) != 0) { + return false; + } + + // empty dict and NULL dict is different + d1 = tv1->v_type == VAR_FUNC ? NULL : tv1->vval.v_partial->pt_dict; + d2 = tv2->v_type == VAR_FUNC ? NULL : tv2->vval.v_partial->pt_dict; + if (d1 == NULL || d2 == NULL) { + if (d1 != d2) { + return false; + } + } else if (!tv_dict_equal(d1, d2, ic, true)) { + return false; + } + + // empty list and no list considered the same + a1 = tv1->v_type == VAR_FUNC ? 0 : tv1->vval.v_partial->pt_argc; + a2 = tv2->v_type == VAR_FUNC ? 0 : tv2->vval.v_partial->pt_argc; + if (a1 != a2) { + return false; + } + for (int i = 0; i < a1; i++) { + if (!tv_equal(tv1->vval.v_partial->pt_argv + i, + tv2->vval.v_partial->pt_argv + i, ic, true)) { + return false; + } + } + return true; +} + +/// Get next (unique) copy ID +/// +/// Used for traversing nested structures e.g. when serializing them or garbage +/// collecting. +int get_copyID(void) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + // CopyID for recursively traversing lists and dicts + // + // This value is needed to avoid endless recursiveness. Last bit is used for + // previous_funccal and normally ignored when comparing. + static int current_copyID = 0; + current_copyID += COPYID_INC; + return current_copyID; +} + +/* + * Garbage collection for lists and dictionaries. + * + * We use reference counts to be able to free most items right away when they + * are no longer used. But for composite items it's possible that it becomes + * unused while the reference count is > 0: When there is a recursive + * reference. Example: + * :let l = [1, 2, 3] + * :let d = {9: l} + * :let l[1] = d + * + * Since this is quite unusual we handle this with garbage collection: every + * once in a while find out which lists and dicts are not referenced from any + * variable. + * + * Here is a good reference text about garbage collection (refers to Python + * but it applies to all reference-counting mechanisms): + * http://python.ca/nas/python/gc/ + */ + +/// Do garbage collection for lists and dicts. +/// +/// @param testing true if called from test_garbagecollect_now(). +/// +/// @return true if some memory was freed. +bool garbage_collect(bool testing) +{ + bool abort = false; +#define ABORTING(func) abort = abort || func + + if (!testing) { + // Only do this once. + want_garbage_collect = false; + may_garbage_collect = false; + garbage_collect_at_exit = false; + } + + // We advance by two (COPYID_INC) because we add one for items referenced + // through previous_funccal. + const int copyID = get_copyID(); + + // 1. Go through all accessible variables and mark all lists and dicts + // with copyID. + + // Don't free variables in the previous_funccal list unless they are only + // referenced through previous_funccal. This must be first, because if + // the item is referenced elsewhere the funccal must not be freed. + ABORTING(set_ref_in_previous_funccal)(copyID); + + // script-local variables + for (int i = 1; i <= ga_scripts.ga_len; ++i) { + ABORTING(set_ref_in_ht)(&SCRIPT_VARS(i), copyID, NULL); + } + + FOR_ALL_BUFFERS(buf) { + // buffer-local variables + ABORTING(set_ref_in_item)(&buf->b_bufvar.di_tv, copyID, NULL, NULL); + // buffer marks (ShaDa additional data) + ABORTING(set_ref_in_fmark)(buf->b_last_cursor, copyID); + ABORTING(set_ref_in_fmark)(buf->b_last_insert, copyID); + ABORTING(set_ref_in_fmark)(buf->b_last_change, copyID); + for (size_t i = 0; i < NMARKS; i++) { + ABORTING(set_ref_in_fmark)(buf->b_namedm[i], copyID); + } + // buffer change list (ShaDa additional data) + for (int i = 0; i < buf->b_changelistlen; i++) { + ABORTING(set_ref_in_fmark)(buf->b_changelist[i], copyID); + } + // buffer ShaDa additional data + ABORTING(set_ref_dict)(buf->additional_data, copyID); + + // buffer callback functions + set_ref_in_callback(&buf->b_prompt_callback, copyID, NULL, NULL); + set_ref_in_callback(&buf->b_prompt_interrupt, copyID, NULL, NULL); + } + + FOR_ALL_TAB_WINDOWS(tp, wp) { + // window-local variables + ABORTING(set_ref_in_item)(&wp->w_winvar.di_tv, copyID, NULL, NULL); + // window jump list (ShaDa additional data) + for (int i = 0; i < wp->w_jumplistlen; i++) { + ABORTING(set_ref_in_fmark)(wp->w_jumplist[i].fmark, copyID); + } + } + if (aucmd_win != NULL) { + ABORTING(set_ref_in_item)(&aucmd_win->w_winvar.di_tv, copyID, NULL, NULL); + } + + // registers (ShaDa additional data) + { + iter_register_T reg_iter = ITER_REGISTER_NULL; + do { + yankreg_T reg; + char name = NUL; + bool is_unnamed = false; + reg_iter = op_global_reg_iter(reg_iter, &name, ®, &is_unnamed); + if (name != NUL) { + ABORTING(set_ref_dict)(reg.additional_data, copyID); + } + } while (reg_iter != ITER_REGISTER_NULL); + } + + // global marks (ShaDa additional data) + { + const void *mark_iter = NULL; + do { + xfmark_T fm; + char name = NUL; + mark_iter = mark_global_iter(mark_iter, &name, &fm); + if (name != NUL) { + ABORTING(set_ref_dict)(fm.fmark.additional_data, copyID); + } + } while (mark_iter != NULL); + } + + // tabpage-local variables + FOR_ALL_TABS(tp) { + ABORTING(set_ref_in_item)(&tp->tp_winvar.di_tv, copyID, NULL, NULL); + } + + // global variables + ABORTING(set_ref_in_ht)(&globvarht, copyID, NULL); + + // function-local variables + ABORTING(set_ref_in_call_stack)(copyID); + + // named functions (matters for closures) + ABORTING(set_ref_in_functions)(copyID); + + // Channels + { + Channel *data; + map_foreach_value(&channels, data, { + set_ref_in_callback_reader(&data->on_data, copyID, NULL, NULL); + set_ref_in_callback_reader(&data->on_stderr, copyID, NULL, NULL); + set_ref_in_callback(&data->on_exit, copyID, NULL, NULL); + }) + } + + // Timers + { + timer_T *timer; + map_foreach_value(&timers, timer, { + set_ref_in_callback(&timer->callback, copyID, NULL, NULL); + }) + } + + // function call arguments, if v:testing is set. + ABORTING(set_ref_in_func_args)(copyID); + + // v: vars + ABORTING(set_ref_in_ht)(&vimvarht, copyID, NULL); + + // history items (ShaDa additional elements) + if (p_hi) { + for (uint8_t i = 0; i < HIST_COUNT; i++) { + const void *iter = NULL; + do { + histentry_T hist; + iter = hist_iter(iter, i, false, &hist); + if (hist.hisstr != NULL) { + ABORTING(set_ref_list)(hist.additional_elements, copyID); + } + } while (iter != NULL); + } + } + + // previously used search/substitute patterns (ShaDa additional data) + { + SearchPattern pat; + get_search_pattern(&pat); + ABORTING(set_ref_dict)(pat.additional_data, copyID); + get_substitute_pattern(&pat); + ABORTING(set_ref_dict)(pat.additional_data, copyID); + } + + // previously used replacement string + { + SubReplacementString sub; + sub_get_replacement(&sub); + ABORTING(set_ref_list)(sub.additional_elements, copyID); + } + + ABORTING(set_ref_in_quickfix)(copyID); + + bool did_free = false; + if (!abort) { + // 2. Free lists and dictionaries that are not referenced. + did_free = free_unref_items(copyID); + + // 3. Check if any funccal can be freed now. + // This may call us back recursively. + did_free = free_unref_funccal(copyID, testing) || did_free; + } else if (p_verbose > 0) { + verb_msg(_("Not enough memory to set references, garbage collection aborted!")); + } +#undef ABORTING + return did_free; +} + +/// Free lists and dictionaries that are no longer referenced. +/// +/// @note This function may only be called from garbage_collect(). +/// +/// @param copyID Free lists/dictionaries that don't have this ID. +/// +/// @return true, if something was freed. +static int free_unref_items(int copyID) +{ + bool did_free = false; + + // Let all "free" functions know that we are here. This means no + // dictionaries, lists, or jobs are to be freed, because we will + // do that here. + tv_in_free_unref_items = true; + + // PASS 1: free the contents of the items. We don't free the items + // themselves yet, so that it is possible to decrement refcount counters. + + // Go through the list of dicts and free items without the copyID. + // Don't free dicts that are referenced internally. + for (dict_T *dd = gc_first_dict; dd != NULL; dd = dd->dv_used_next) { + if ((dd->dv_copyID & COPYID_MASK) != (copyID & COPYID_MASK)) { + // Free the Dictionary and ordinary items it contains, but don't + // recurse into Lists and Dictionaries, they will be in the list + // of dicts or list of lists. + tv_dict_free_contents(dd); + did_free = true; + } + } + + // Go through the list of lists and free items without the copyID. + // But don't free a list that has a watcher (used in a for loop), these + // are not referenced anywhere. + for (list_T *ll = gc_first_list; ll != NULL; ll = ll->lv_used_next) { + if ((tv_list_copyid(ll) & COPYID_MASK) != (copyID & COPYID_MASK) + && !tv_list_has_watchers(ll)) { + // Free the List and ordinary items it contains, but don't recurse + // into Lists and Dictionaries, they will be in the list of dicts + // or list of lists. + tv_list_free_contents(ll); + did_free = true; + } + } + + // PASS 2: free the items themselves. + dict_T *dd_next; + for (dict_T *dd = gc_first_dict; dd != NULL; dd = dd_next) { + dd_next = dd->dv_used_next; + if ((dd->dv_copyID & COPYID_MASK) != (copyID & COPYID_MASK)) { + tv_dict_free_dict(dd); + } + } + + list_T *ll_next; + for (list_T *ll = gc_first_list; ll != NULL; ll = ll_next) { + ll_next = ll->lv_used_next; + if ((ll->lv_copyID & COPYID_MASK) != (copyID & COPYID_MASK) + && !tv_list_has_watchers(ll)) { + // Free the List and ordinary items it contains, but don't recurse + // into Lists and Dictionaries, they will be in the list of dicts + // or list of lists. + tv_list_free_list(ll); + } + } + tv_in_free_unref_items = false; + return did_free; +} + +/// Mark all lists and dicts referenced through hashtab "ht" with "copyID". +/// +/// @param ht Hashtab content will be marked. +/// @param copyID New mark for lists and dicts. +/// @param list_stack Used to add lists to be marked. Can be NULL. +/// +/// @returns true if setting references failed somehow. +bool set_ref_in_ht(hashtab_T *ht, int copyID, list_stack_T **list_stack) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + bool abort = false; + ht_stack_T *ht_stack = NULL; + + hashtab_T *cur_ht = ht; + for (;;) { + if (!abort) { + // Mark each item in the hashtab. If the item contains a hashtab + // it is added to ht_stack, if it contains a list it is added to + // list_stack. + HASHTAB_ITER(cur_ht, hi, { + abort = abort || set_ref_in_item(&TV_DICT_HI2DI(hi)->di_tv, copyID, &ht_stack, list_stack); + }); + } + + if (ht_stack == NULL) { + break; + } + + // take an item from the stack + cur_ht = ht_stack->ht; + ht_stack_T *tempitem = ht_stack; + ht_stack = ht_stack->prev; + xfree(tempitem); + } + + return abort; +} + +/// Mark all lists and dicts referenced through list "l" with "copyID". +/// +/// @param l List content will be marked. +/// @param copyID New mark for lists and dicts. +/// @param ht_stack Used to add hashtabs to be marked. Can be NULL. +/// +/// @returns true if setting references failed somehow. +bool set_ref_in_list(list_T *l, int copyID, ht_stack_T **ht_stack) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + bool abort = false; + list_stack_T *list_stack = NULL; + + list_T *cur_l = l; + for (;;) { + // Mark each item in the list. If the item contains a hashtab + // it is added to ht_stack, if it contains a list it is added to + // list_stack. + TV_LIST_ITER(cur_l, li, { + if (abort) { + break; + } + abort = set_ref_in_item(TV_LIST_ITEM_TV(li), copyID, ht_stack, + &list_stack); + }); + + if (list_stack == NULL) { + break; + } + + // take an item from the stack + cur_l = list_stack->list; + list_stack_T *tempitem = list_stack; + list_stack = list_stack->prev; + xfree(tempitem); + } + + return abort; +} + +/// Mark all lists and dicts referenced through typval "tv" with "copyID". +/// +/// @param tv Typval content will be marked. +/// @param copyID New mark for lists and dicts. +/// @param ht_stack Used to add hashtabs to be marked. Can be NULL. +/// @param list_stack Used to add lists to be marked. Can be NULL. +/// +/// @returns true if setting references failed somehow. +bool set_ref_in_item(typval_T *tv, int copyID, ht_stack_T **ht_stack, list_stack_T **list_stack) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + bool abort = false; + + switch (tv->v_type) { + case VAR_DICT: { + dict_T *dd = tv->vval.v_dict; + if (dd != NULL && dd->dv_copyID != copyID) { + // Didn't see this dict yet. + dd->dv_copyID = copyID; + if (ht_stack == NULL) { + abort = set_ref_in_ht(&dd->dv_hashtab, copyID, list_stack); + } else { + ht_stack_T *const newitem = xmalloc(sizeof(ht_stack_T)); + newitem->ht = &dd->dv_hashtab; + newitem->prev = *ht_stack; + *ht_stack = newitem; + } + + QUEUE *w = NULL; + DictWatcher *watcher = NULL; + QUEUE_FOREACH(w, &dd->watchers, { + watcher = tv_dict_watcher_node_data(w); + set_ref_in_callback(&watcher->callback, copyID, ht_stack, list_stack); + }) + } + break; + } + + case VAR_LIST: { + list_T *ll = tv->vval.v_list; + if (ll != NULL && ll->lv_copyID != copyID) { + // Didn't see this list yet. + ll->lv_copyID = copyID; + if (list_stack == NULL) { + abort = set_ref_in_list(ll, copyID, ht_stack); + } else { + list_stack_T *const newitem = xmalloc(sizeof(list_stack_T)); + newitem->list = ll; + newitem->prev = *list_stack; + *list_stack = newitem; + } + } + break; + } + + case VAR_PARTIAL: { + partial_T *pt = tv->vval.v_partial; + + // A partial does not have a copyID, because it cannot contain itself. + if (pt != NULL) { + abort = set_ref_in_func(pt->pt_name, pt->pt_func, copyID); + if (pt->pt_dict != NULL) { + typval_T dtv; + + dtv.v_type = VAR_DICT; + dtv.vval.v_dict = pt->pt_dict; + abort = abort || set_ref_in_item(&dtv, copyID, ht_stack, list_stack); + } + + for (int i = 0; i < pt->pt_argc; i++) { + abort = abort || set_ref_in_item(&pt->pt_argv[i], copyID, + ht_stack, list_stack); + } + } + break; + } + case VAR_FUNC: + abort = set_ref_in_func((char_u *)tv->vval.v_string, NULL, copyID); + break; + case VAR_UNKNOWN: + case VAR_BOOL: + case VAR_SPECIAL: + case VAR_FLOAT: + case VAR_NUMBER: + case VAR_STRING: + case VAR_BLOB: + break; + } + return abort; +} + +/// Mark all lists and dicts referenced in given mark +/// +/// @return true if setting references failed somehow. +static inline bool set_ref_in_fmark(fmark_T fm, int copyID) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (fm.additional_data != NULL + && fm.additional_data->dv_copyID != copyID) { + fm.additional_data->dv_copyID = copyID; + return set_ref_in_ht(&fm.additional_data->dv_hashtab, copyID, NULL); + } + return false; +} + +/// Mark all lists and dicts referenced in given list and the list itself +/// +/// @return true if setting references failed somehow. +static inline bool set_ref_list(list_T *list, int copyID) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (list != NULL) { + typval_T tv = (typval_T) { + .v_type = VAR_LIST, + .vval = { .v_list = list } + }; + return set_ref_in_item(&tv, copyID, NULL, NULL); + } + return false; +} + +/// Mark all lists and dicts referenced in given dict and the dict itself +/// +/// @return true if setting references failed somehow. +static inline bool set_ref_dict(dict_T *dict, int copyID) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (dict != NULL) { + typval_T tv = (typval_T) { + .v_type = VAR_DICT, + .vval = { .v_dict = dict } + }; + return set_ref_in_item(&tv, copyID, NULL, NULL); + } + return false; +} + +/// Get the key for #{key: val} into "tv" and advance "arg". +/// +/// @return FAIL when there is no valid key. +static int get_literal_key(char **arg, typval_T *tv) + FUNC_ATTR_NONNULL_ALL +{ + char *p; + + if (!ASCII_ISALNUM(**arg) && **arg != '_' && **arg != '-') { + return FAIL; + } + for (p = *arg; ASCII_ISALNUM(*p) || *p == '_' || *p == '-'; p++) {} + tv->v_type = VAR_STRING; + tv->vval.v_string = xstrnsave(*arg, (size_t)(p - *arg)); + + *arg = skipwhite(p); + return OK; +} + +/// Allocate a variable for a Dictionary and fill it from "*arg". +/// "literal" is true for #{key: val} +/// +/// @return OK or FAIL. Returns NOTDONE for {expr}. +static int dict_get_tv(char **arg, typval_T *rettv, int evaluate, bool literal) +{ + dict_T *d = NULL; + typval_T tvkey; + typval_T tv; + char *key = NULL; + dictitem_T *item; + char *start = skipwhite(*arg + 1); + char buf[NUMBUFLEN]; + + /* + * First check if it's not a curly-braces thing: {expr}. + * Must do this without evaluating, otherwise a function may be called + * twice. Unfortunately this means we need to call eval1() twice for the + * first item. + * But {} is an empty Dictionary. + */ + if (*start != '}') { + if (eval1(&start, &tv, false) == FAIL) { // recursive! + return FAIL; + } + if (*skipwhite(start) == '}') { + return NOTDONE; + } + } + + if (evaluate) { + d = tv_dict_alloc(); + } + tvkey.v_type = VAR_UNKNOWN; + tv.v_type = VAR_UNKNOWN; + + *arg = skipwhite(*arg + 1); + while (**arg != '}' && **arg != NUL) { + if ((literal + ? get_literal_key(arg, &tvkey) + : eval1(arg, &tvkey, evaluate)) == FAIL) { // recursive! + goto failret; + } + if (**arg != ':') { + semsg(_("E720: Missing colon in Dictionary: %s"), *arg); + tv_clear(&tvkey); + goto failret; + } + if (evaluate) { + key = (char *)tv_get_string_buf_chk(&tvkey, buf); + if (key == NULL) { + // "key" is NULL when tv_get_string_buf_chk() gave an errmsg + tv_clear(&tvkey); + goto failret; + } + } + + *arg = skipwhite(*arg + 1); + if (eval1(arg, &tv, evaluate) == FAIL) { // Recursive! + if (evaluate) { + tv_clear(&tvkey); + } + goto failret; + } + if (evaluate) { + item = tv_dict_find(d, (const char *)key, -1); + if (item != NULL) { + semsg(_("E721: Duplicate key in Dictionary: \"%s\""), key); + tv_clear(&tvkey); + tv_clear(&tv); + goto failret; + } + item = tv_dict_item_alloc((const char *)key); + item->di_tv = tv; + item->di_tv.v_lock = VAR_UNLOCKED; + if (tv_dict_add(d, item) == FAIL) { + tv_dict_item_free(item); + } + } + tv_clear(&tvkey); + + if (**arg == '}') { + break; + } + if (**arg != ',') { + semsg(_("E722: Missing comma in Dictionary: %s"), *arg); + goto failret; + } + *arg = skipwhite(*arg + 1); + } + + if (**arg != '}') { + semsg(_("E723: Missing end of Dictionary '}': %s"), *arg); +failret: + if (d != NULL) { + tv_dict_free(d); + } + return FAIL; + } + + *arg = skipwhite(*arg + 1); + if (evaluate) { + tv_dict_set_ret(rettv, d); + } + + return OK; +} + +/// Convert the string to a floating point number +/// +/// This uses strtod(). setlocale(LC_NUMERIC, "C") has been used earlier to +/// make sure this always uses a decimal point. +/// +/// @param[in] text String to convert. +/// @param[out] ret_value Location where conversion result is saved. +/// +/// @return Length of the text that was consumed. +size_t string2float(const char *const text, float_T *const ret_value) + FUNC_ATTR_NONNULL_ALL +{ + char *s = NULL; + + // MS-Windows does not deal with "inf" and "nan" properly + if (STRNICMP(text, "inf", 3) == 0) { + *ret_value = (float_T)INFINITY; + return 3; + } + if (STRNICMP(text, "-inf", 3) == 0) { + *ret_value = (float_T) - INFINITY; + return 4; + } + if (STRNICMP(text, "nan", 3) == 0) { + *ret_value = (float_T)NAN; + return 3; + } + *ret_value = strtod(text, &s); + return (size_t)(s - text); +} + +/// Get the value of an environment variable. +/// +/// If the environment variable was not set, silently assume it is empty. +/// +/// @param arg Points to the '$'. It is advanced to after the name. +/// +/// @return FAIL if the name is invalid. +static int get_env_tv(char **arg, typval_T *rettv, int evaluate) +{ + char *name; + char *string = NULL; + int len; + int cc; + + ++*arg; + name = *arg; + len = get_env_len((const char **)arg); + + if (evaluate) { + if (len == 0) { + return FAIL; // Invalid empty name. + } + cc = (char_u)name[len]; + name[len] = NUL; + // First try vim_getenv(), fast for normal environment vars. + string = vim_getenv(name); + if (string == NULL || *string == NUL) { + xfree(string); + + // Next try expanding things like $VIM and ${HOME}. + string = expand_env_save(name - 1); + if (string != NULL && *string == '$') { + XFREE_CLEAR(string); + } + } + name[len] = (char)cc; + rettv->v_type = VAR_STRING; + rettv->vval.v_string = string; + } + + return OK; +} + +/// Get the argument list for a given window +void get_arglist_as_rettv(aentry_T *arglist, int argcount, typval_T *rettv) +{ + tv_list_alloc_ret(rettv, argcount); + if (arglist != NULL) { + for (int idx = 0; idx < argcount; idx++) { + tv_list_append_string(rettv->vval.v_list, + (const char *)alist_name(&arglist[idx]), -1); + } + } +} + +/// Add an assert error to v:errors. +void assert_error(garray_T *gap) +{ + struct vimvar *vp = &vimvars[VV_ERRORS]; + + if (vp->vv_type != VAR_LIST || vimvars[VV_ERRORS].vv_list == NULL) { + // Make sure v:errors is a list. + set_vim_var_list(VV_ERRORS, tv_list_alloc(1)); + } + tv_list_append_string(vimvars[VV_ERRORS].vv_list, + (const char *)gap->ga_data, (ptrdiff_t)gap->ga_len); +} + +/// Find a window: When using a Window ID in any tab page, when using a number +/// in the current tab page. +win_T *find_win_by_nr_or_id(typval_T *vp) +{ + int nr = (int)tv_get_number_chk(vp, NULL); + + if (nr >= LOWEST_WIN_ID) { + return win_id2wp((int)tv_get_number(vp)); + } + + return find_win_by_nr(vp, NULL); +} + +/// Implementation of map() and filter(). +void filter_map(typval_T *argvars, typval_T *rettv, int map) +{ + typval_T *expr; + list_T *l = NULL; + dictitem_T *di; + hashtab_T *ht; + hashitem_T *hi; + dict_T *d = NULL; + typval_T save_val; + typval_T save_key; + blob_T *b = NULL; + int rem = false; + int todo; + char *ermsg = map ? "map()" : "filter()"; + const char *const arg_errmsg = (map + ? N_("map() argument") + : N_("filter() argument")); + int save_did_emsg; + int idx = 0; + + if (argvars[0].v_type == VAR_BLOB) { + tv_copy(&argvars[0], rettv); + if ((b = argvars[0].vval.v_blob) == NULL) { + return; + } + } else if (argvars[0].v_type == VAR_LIST) { + tv_copy(&argvars[0], rettv); + if ((l = argvars[0].vval.v_list) == NULL + || (!map + && var_check_lock(tv_list_locked(l), arg_errmsg, TV_TRANSLATE))) { + return; + } + } else if (argvars[0].v_type == VAR_DICT) { + tv_copy(&argvars[0], rettv); + if ((d = argvars[0].vval.v_dict) == NULL + || (!map && var_check_lock(d->dv_lock, arg_errmsg, TV_TRANSLATE))) { + return; + } + } else { + semsg(_(e_listdictblobarg), ermsg); + return; + } + + expr = &argvars[1]; + // On type errors, the preceding call has already displayed an error + // message. Avoid a misleading error message for an empty string that + // was not passed as argument. + if (expr->v_type != VAR_UNKNOWN) { + prepare_vimvar(VV_VAL, &save_val); + + // We reset "did_emsg" to be able to detect whether an error + // occurred during evaluation of the expression. + save_did_emsg = did_emsg; + did_emsg = FALSE; + + prepare_vimvar(VV_KEY, &save_key); + if (argvars[0].v_type == VAR_DICT) { + vimvars[VV_KEY].vv_type = VAR_STRING; + + const VarLockStatus prev_lock = d->dv_lock; + if (map && d->dv_lock == VAR_UNLOCKED) { + d->dv_lock = VAR_LOCKED; + } + ht = &d->dv_hashtab; + hash_lock(ht); + todo = (int)ht->ht_used; + for (hi = ht->ht_array; todo > 0; ++hi) { + if (!HASHITEM_EMPTY(hi)) { + --todo; + + di = TV_DICT_HI2DI(hi); + if (map + && (var_check_lock(di->di_tv.v_lock, arg_errmsg, TV_TRANSLATE) + || var_check_ro(di->di_flags, arg_errmsg, TV_TRANSLATE))) { + break; + } + + vimvars[VV_KEY].vv_str = (char *)vim_strsave(di->di_key); + int r = filter_map_one(&di->di_tv, expr, map, &rem); + tv_clear(&vimvars[VV_KEY].vv_tv); + if (r == FAIL || did_emsg) { + break; + } + if (!map && rem) { + if (var_check_fixed(di->di_flags, arg_errmsg, TV_TRANSLATE) + || var_check_ro(di->di_flags, arg_errmsg, TV_TRANSLATE)) { + break; + } + tv_dict_item_remove(d, di); + } + } + } + hash_unlock(ht); + d->dv_lock = prev_lock; + } else if (argvars[0].v_type == VAR_BLOB) { + vimvars[VV_KEY].vv_type = VAR_NUMBER; + + for (int i = 0; i < b->bv_ga.ga_len; i++) { + typval_T tv; + tv.v_type = VAR_NUMBER; + const varnumber_T val = tv_blob_get(b, i); + tv.vval.v_number = val; + vimvars[VV_KEY].vv_nr = idx; + if (filter_map_one(&tv, expr, map, &rem) == FAIL || did_emsg) { + break; + } + if (tv.v_type != VAR_NUMBER) { + emsg(_(e_invalblob)); + return; + } + if (map) { + if (tv.vval.v_number != val) { + tv_blob_set(b, i, (char_u)tv.vval.v_number); + } + } else if (rem) { + char *const p = argvars[0].vval.v_blob->bv_ga.ga_data; + memmove(p + i, p + i + 1, (size_t)(b->bv_ga.ga_len - i - 1)); + b->bv_ga.ga_len--; + i--; + } + idx++; + } + } else { + assert(argvars[0].v_type == VAR_LIST); + vimvars[VV_KEY].vv_type = VAR_NUMBER; + + const VarLockStatus prev_lock = tv_list_locked(l); + if (map && tv_list_locked(l) == VAR_UNLOCKED) { + tv_list_set_lock(l, VAR_LOCKED); + } + for (listitem_T *li = tv_list_first(l); li != NULL;) { + if (map + && var_check_lock(TV_LIST_ITEM_TV(li)->v_lock, arg_errmsg, + TV_TRANSLATE)) { + break; + } + vimvars[VV_KEY].vv_nr = idx; + if (filter_map_one(TV_LIST_ITEM_TV(li), expr, map, &rem) == FAIL + || did_emsg) { + break; + } + if (!map && rem) { + li = tv_list_item_remove(l, li); + } else { + li = TV_LIST_ITEM_NEXT(l, li); + } + idx++; + } + tv_list_set_lock(l, prev_lock); + } + + restore_vimvar(VV_KEY, &save_key); + restore_vimvar(VV_VAL, &save_val); + + did_emsg |= save_did_emsg; + } +} + +static int filter_map_one(typval_T *tv, typval_T *expr, int map, int *remp) + FUNC_ATTR_NONNULL_ARG(1, 2) +{ + typval_T rettv; + typval_T argv[3]; + int retval = FAIL; + + tv_copy(tv, &vimvars[VV_VAL].vv_tv); + argv[0] = vimvars[VV_KEY].vv_tv; + argv[1] = vimvars[VV_VAL].vv_tv; + if (eval_expr_typval(expr, argv, 2, &rettv) == FAIL) { + goto theend; + } + if (map) { + // map(): replace the list item value. + tv_clear(tv); + rettv.v_lock = VAR_UNLOCKED; + *tv = rettv; + } else { + bool error = false; + + // filter(): when expr is zero remove the item + *remp = (tv_get_number_chk(&rettv, &error) == 0); + tv_clear(&rettv); + // On type error, nothing has been removed; return FAIL to stop the + // loop. The error message was given by tv_get_number_chk(). + if (error) { + goto theend; + } + } + retval = OK; +theend: + tv_clear(&vimvars[VV_VAL].vv_tv); + return retval; +} + +void common_function(typval_T *argvars, typval_T *rettv, bool is_funcref, FunPtr fptr) +{ + char *s; + char *name; + bool use_string = false; + partial_T *arg_pt = NULL; + char *trans_name = NULL; + + if (argvars[0].v_type == VAR_FUNC) { + // function(MyFunc, [arg], dict) + s = argvars[0].vval.v_string; + } else if (argvars[0].v_type == VAR_PARTIAL + && argvars[0].vval.v_partial != NULL) { + // function(dict.MyFunc, [arg]) + arg_pt = argvars[0].vval.v_partial; + s = partial_name(arg_pt); + // TODO(bfredl): do the entire nlua_is_table_from_lua dance + } else { + // function('MyFunc', [arg], dict) + s = (char *)tv_get_string(&argvars[0]); + use_string = true; + } + + if ((use_string && vim_strchr(s, AUTOLOAD_CHAR) == NULL) || is_funcref) { + name = s; + trans_name = (char *)trans_function_name((char_u **)&name, false, + TFN_INT | TFN_QUIET | TFN_NO_AUTOLOAD + | TFN_NO_DEREF, NULL, NULL); + if (*name != NUL) { + s = NULL; + } + } + if (s == NULL || *s == NUL || (use_string && ascii_isdigit(*s)) + || (is_funcref && trans_name == NULL)) { + semsg(_(e_invarg2), (use_string + ? tv_get_string(&argvars[0]) + : (const char *)s)); + // Don't check an autoload name for existence here. + } else if (trans_name != NULL + && (is_funcref + ? find_func((char_u *)trans_name) == NULL + : !translated_function_exists((const char *)trans_name))) { + semsg(_("E700: Unknown function: %s"), s); + } else { + int dict_idx = 0; + int arg_idx = 0; + list_T *list = NULL; + if (STRNCMP(s, "s:", 2) == 0 || STRNCMP(s, "<SID>", 5) == 0) { + char sid_buf[25]; + int off = *s == 's' ? 2 : 5; + + // Expand s: and <SID> into <SNR>nr_, so that the function can + // also be called from another script. Using trans_function_name() + // would also work, but some plugins depend on the name being + // printable text. + snprintf(sid_buf, sizeof(sid_buf), "<SNR>%" PRId64 "_", + (int64_t)current_sctx.sc_sid); + name = xmalloc(STRLEN(sid_buf) + STRLEN(s + off) + 1); + STRCPY(name, sid_buf); + STRCAT(name, s + off); + } else { + name = xstrdup(s); + } + + if (argvars[1].v_type != VAR_UNKNOWN) { + if (argvars[2].v_type != VAR_UNKNOWN) { + // function(name, [args], dict) + arg_idx = 1; + dict_idx = 2; + } else if (argvars[1].v_type == VAR_DICT) { + // function(name, dict) + dict_idx = 1; + } else { + // function(name, [args]) + arg_idx = 1; + } + if (dict_idx > 0) { + if (argvars[dict_idx].v_type != VAR_DICT) { + emsg(_("E922: expected a dict")); + xfree(name); + goto theend; + } + if (argvars[dict_idx].vval.v_dict == NULL) { + dict_idx = 0; + } + } + if (arg_idx > 0) { + if (argvars[arg_idx].v_type != VAR_LIST) { + emsg(_("E923: Second argument of function() must be " + "a list or a dict")); + xfree(name); + goto theend; + } + list = argvars[arg_idx].vval.v_list; + if (tv_list_len(list) == 0) { + arg_idx = 0; + } else if (tv_list_len(list) > MAX_FUNC_ARGS) { + emsg_funcname((char *)e_toomanyarg, (char_u *)s); + xfree(name); + goto theend; + } + } + } + if (dict_idx > 0 || arg_idx > 0 || arg_pt != NULL || is_funcref) { + partial_T *const pt = xcalloc(1, sizeof(*pt)); + + // result is a VAR_PARTIAL + if (arg_idx > 0 || (arg_pt != NULL && arg_pt->pt_argc > 0)) { + const int arg_len = (arg_pt == NULL ? 0 : arg_pt->pt_argc); + const int lv_len = tv_list_len(list); + + pt->pt_argc = arg_len + lv_len; + pt->pt_argv = xmalloc(sizeof(pt->pt_argv[0]) * (size_t)pt->pt_argc); + int i = 0; + for (; i < arg_len; i++) { + tv_copy(&arg_pt->pt_argv[i], &pt->pt_argv[i]); + } + if (lv_len > 0) { + TV_LIST_ITER(list, li, { + tv_copy(TV_LIST_ITEM_TV(li), &pt->pt_argv[i++]); + }); + } + } + + // For "function(dict.func, [], dict)" and "func" is a partial + // use "dict". That is backwards compatible. + if (dict_idx > 0) { + // The dict is bound explicitly, pt_auto is false + pt->pt_dict = argvars[dict_idx].vval.v_dict; + (pt->pt_dict->dv_refcount)++; + } else if (arg_pt != NULL) { + // If the dict was bound automatically the result is also + // bound automatically. + pt->pt_dict = arg_pt->pt_dict; + pt->pt_auto = arg_pt->pt_auto; + if (pt->pt_dict != NULL) { + (pt->pt_dict->dv_refcount)++; + } + } + + pt->pt_refcount = 1; + if (arg_pt != NULL && arg_pt->pt_func != NULL) { + pt->pt_func = arg_pt->pt_func; + func_ptr_ref(pt->pt_func); + xfree(name); + } else if (is_funcref) { + pt->pt_func = find_func((char_u *)trans_name); + func_ptr_ref(pt->pt_func); + xfree(name); + } else { + pt->pt_name = (char_u *)name; + func_ref((char_u *)name); + } + + rettv->v_type = VAR_PARTIAL; + rettv->vval.v_partial = pt; + } else { + // result is a VAR_FUNC + rettv->v_type = VAR_FUNC; + rettv->vval.v_string = name; + func_ref((char_u *)name); + } + } +theend: + xfree(trans_name); +} + +/// @return buffer options, variables and other attributes in a dictionary. +dict_T *get_buffer_info(buf_T *buf) +{ + dict_T *const dict = tv_dict_alloc(); + + tv_dict_add_nr(dict, S_LEN("bufnr"), buf->b_fnum); + tv_dict_add_str(dict, S_LEN("name"), + buf->b_ffname != NULL ? (const char *)buf->b_ffname : ""); + tv_dict_add_nr(dict, S_LEN("lnum"), + buf == curbuf ? curwin->w_cursor.lnum : buflist_findlnum(buf)); + tv_dict_add_nr(dict, S_LEN("linecount"), buf->b_ml.ml_line_count); + tv_dict_add_nr(dict, S_LEN("loaded"), buf->b_ml.ml_mfp != NULL); + tv_dict_add_nr(dict, S_LEN("listed"), buf->b_p_bl); + tv_dict_add_nr(dict, S_LEN("changed"), bufIsChanged(buf)); + tv_dict_add_nr(dict, S_LEN("changedtick"), buf_get_changedtick(buf)); + tv_dict_add_nr(dict, S_LEN("hidden"), + buf->b_ml.ml_mfp != NULL && buf->b_nwindows == 0); + + // Get a reference to buffer variables + tv_dict_add_dict(dict, S_LEN("variables"), buf->b_vars); + + // List of windows displaying this buffer + list_T *const windows = tv_list_alloc(kListLenMayKnow); + FOR_ALL_TAB_WINDOWS(tp, wp) { + if (wp->w_buffer == buf) { + tv_list_append_number(windows, (varnumber_T)wp->handle); + } + } + tv_dict_add_list(dict, S_LEN("windows"), windows); + + if (buf->b_signlist != NULL) { + // List of signs placed in this buffer + tv_dict_add_list(dict, S_LEN("signs"), get_buffer_signs(buf)); + } + + tv_dict_add_nr(dict, S_LEN("lastused"), buf->b_last_used); + + return dict; +} + +/// Get the line number from VimL object +/// +/// @note Unlike tv_get_lnum(), this one supports only "$" special string. +/// +/// @param[in] tv Object to get value from. Is expected to be a number or +/// a special string "$". +/// @param[in] buf Buffer to take last line number from in case tv is "$". May +/// be NULL, in this case "$" results in zero return. +/// +/// @return Line number or 0 in case of error. +linenr_T tv_get_lnum_buf(const typval_T *const tv, const buf_T *const buf) + FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (tv->v_type == VAR_STRING + && tv->vval.v_string != NULL + && tv->vval.v_string[0] == '$' + && buf != NULL) { + return buf->b_ml.ml_line_count; + } + return (linenr_T)tv_get_number_chk(tv, NULL); +} + +void get_qf_loc_list(int is_qf, win_T *wp, typval_T *what_arg, typval_T *rettv) +{ + if (what_arg->v_type == VAR_UNKNOWN) { + tv_list_alloc_ret(rettv, kListLenMayKnow); + if (is_qf || wp != NULL) { + (void)get_errorlist(NULL, wp, -1, 0, rettv->vval.v_list); + } + } else { + tv_dict_alloc_ret(rettv); + if (is_qf || wp != NULL) { + if (what_arg->v_type == VAR_DICT) { + dict_T *d = what_arg->vval.v_dict; + + if (d != NULL) { + qf_get_properties(wp, d, rettv->vval.v_dict); + } + } else { + emsg(_(e_dictreq)); + } + } + } +} + +/// @return information (variables, options, etc.) about a tab page +/// as a dictionary. +dict_T *get_tabpage_info(tabpage_T *tp, int tp_idx) +{ + dict_T *const dict = tv_dict_alloc(); + + tv_dict_add_nr(dict, S_LEN("tabnr"), tp_idx); + + list_T *const l = tv_list_alloc(kListLenMayKnow); + FOR_ALL_WINDOWS_IN_TAB(wp, tp) { + tv_list_append_number(l, (varnumber_T)wp->handle); + } + tv_dict_add_list(dict, S_LEN("windows"), l); + + // Make a reference to tabpage variables + tv_dict_add_dict(dict, S_LEN("variables"), tp->tp_vars); + + return dict; +} + +/// @return information about a window as a dictionary. +dict_T *get_win_info(win_T *wp, int16_t tpnr, int16_t winnr) +{ + dict_T *const dict = tv_dict_alloc(); + + // make sure w_botline is valid + validate_botline(wp); + + tv_dict_add_nr(dict, S_LEN("tabnr"), tpnr); + tv_dict_add_nr(dict, S_LEN("winnr"), winnr); + tv_dict_add_nr(dict, S_LEN("winid"), wp->handle); + tv_dict_add_nr(dict, S_LEN("height"), wp->w_height); + tv_dict_add_nr(dict, S_LEN("winrow"), wp->w_winrow + 1); + tv_dict_add_nr(dict, S_LEN("topline"), wp->w_topline); + tv_dict_add_nr(dict, S_LEN("botline"), wp->w_botline - 1); + tv_dict_add_nr(dict, S_LEN("winbar"), wp->w_winbar_height); + tv_dict_add_nr(dict, S_LEN("width"), wp->w_width); + tv_dict_add_nr(dict, S_LEN("bufnr"), wp->w_buffer->b_fnum); + tv_dict_add_nr(dict, S_LEN("wincol"), wp->w_wincol + 1); + tv_dict_add_nr(dict, S_LEN("textoff"), win_col_off(wp)); + tv_dict_add_nr(dict, S_LEN("terminal"), bt_terminal(wp->w_buffer)); + tv_dict_add_nr(dict, S_LEN("quickfix"), bt_quickfix(wp->w_buffer)); + tv_dict_add_nr(dict, S_LEN("loclist"), + (bt_quickfix(wp->w_buffer) && wp->w_llist_ref != NULL)); + + // Add a reference to window variables + tv_dict_add_dict(dict, S_LEN("variables"), wp->w_vars); + + return dict; +} + +/// Find window specified by "vp" in tabpage "tp". +/// +/// @param tp NULL for current tab page +win_T *find_win_by_nr(typval_T *vp, tabpage_T *tp) +{ + int nr = (int)tv_get_number_chk(vp, NULL); + + if (nr < 0) { + return NULL; + } + + if (nr == 0) { + return curwin; + } + + // This method accepts NULL as an alias for curtab. + if (tp == NULL) { + tp = curtab; + } + + FOR_ALL_WINDOWS_IN_TAB(wp, tp) { + if (nr >= LOWEST_WIN_ID) { + if (wp->handle == nr) { + return wp; + } + } else if (--nr <= 0) { + return wp; + } + } + return NULL; +} + +/// Find window specified by "wvp" in tabpage "tvp". +win_T *find_tabwin(typval_T *wvp, typval_T *tvp) +{ + win_T *wp = NULL; + tabpage_T *tp = NULL; + + if (wvp->v_type != VAR_UNKNOWN) { + if (tvp->v_type != VAR_UNKNOWN) { + long n = tv_get_number(tvp); + if (n >= 0) { + tp = find_tabpage((int)n); + } + } else { + tp = curtab; + } + + if (tp != NULL) { + wp = find_win_by_nr(wvp, tp); + } + } else { + wp = curwin; + } + + return wp; +} + +/// This function is used by f_input() and f_inputdialog() functions. The third +/// argument to f_input() specifies the type of completion to use at the +/// prompt. The third argument to f_inputdialog() specifies the value to return +/// when the user cancels the prompt. +void get_user_input(const typval_T *const argvars, typval_T *const rettv, const bool inputdialog, + const bool secret) + FUNC_ATTR_NONNULL_ALL +{ + rettv->v_type = VAR_STRING; + rettv->vval.v_string = NULL; + + const char *prompt = ""; + const char *defstr = ""; + typval_T *cancelreturn = NULL; + typval_T cancelreturn_strarg2 = TV_INITIAL_VALUE; + const char *xp_name = NULL; + Callback input_callback = { .type = kCallbackNone }; + char prompt_buf[NUMBUFLEN]; + char defstr_buf[NUMBUFLEN]; + char cancelreturn_buf[NUMBUFLEN]; + char xp_name_buf[NUMBUFLEN]; + char def[1] = { 0 }; + if (argvars[0].v_type == VAR_DICT) { + if (argvars[1].v_type != VAR_UNKNOWN) { + emsg(_("E5050: {opts} must be the only argument")); + return; + } + dict_T *const dict = argvars[0].vval.v_dict; + prompt = tv_dict_get_string_buf_chk(dict, S_LEN("prompt"), prompt_buf, ""); + if (prompt == NULL) { + return; + } + defstr = tv_dict_get_string_buf_chk(dict, S_LEN("default"), defstr_buf, ""); + if (defstr == NULL) { + return; + } + dictitem_T *cancelreturn_di = tv_dict_find(dict, S_LEN("cancelreturn")); + if (cancelreturn_di != NULL) { + cancelreturn = &cancelreturn_di->di_tv; + } + xp_name = tv_dict_get_string_buf_chk(dict, S_LEN("completion"), + xp_name_buf, def); + if (xp_name == NULL) { // error + return; + } + if (xp_name == def) { // default to NULL + xp_name = NULL; + } + if (!tv_dict_get_callback(dict, S_LEN("highlight"), &input_callback)) { + return; + } + } else { + prompt = tv_get_string_buf_chk(&argvars[0], prompt_buf); + if (prompt == NULL) { + return; + } + if (argvars[1].v_type != VAR_UNKNOWN) { + defstr = tv_get_string_buf_chk(&argvars[1], defstr_buf); + if (defstr == NULL) { + return; + } + if (argvars[2].v_type != VAR_UNKNOWN) { + const char *const strarg2 = tv_get_string_buf_chk(&argvars[2], cancelreturn_buf); + if (strarg2 == NULL) { + return; + } + if (inputdialog) { + cancelreturn_strarg2.v_type = VAR_STRING; + cancelreturn_strarg2.vval.v_string = (char *)strarg2; + cancelreturn = &cancelreturn_strarg2; + } else { + xp_name = strarg2; + } + } + } + } + + int xp_type = EXPAND_NOTHING; + char *xp_arg = NULL; + if (xp_name != NULL) { + // input() with a third argument: completion + const int xp_namelen = (int)strlen(xp_name); + + uint32_t argt = 0; + if (parse_compl_arg(xp_name, xp_namelen, &xp_type, + &argt, &xp_arg) == FAIL) { + return; + } + } + + const bool cmd_silent_save = cmd_silent; + + cmd_silent = false; // Want to see the prompt. + // Only the part of the message after the last NL is considered as + // prompt for the command line, unlsess cmdline is externalized + const char *p = prompt; + if (!ui_has(kUICmdline)) { + const char *lastnl = strrchr(prompt, '\n'); + if (lastnl != NULL) { + p = lastnl + 1; + msg_start(); + msg_clr_eos(); + msg_puts_attr_len(prompt, p - prompt, echo_attr); + msg_didout = false; + msg_starthere(); + } + } + cmdline_row = msg_row; + + stuffReadbuffSpec(defstr); + + const int save_ex_normal_busy = ex_normal_busy; + ex_normal_busy = 0; + rettv->vval.v_string = getcmdline_prompt(secret ? NUL : '@', p, echo_attr, xp_type, xp_arg, + input_callback); + ex_normal_busy = save_ex_normal_busy; + callback_free(&input_callback); + + if (rettv->vval.v_string == NULL && cancelreturn != NULL) { + tv_copy(cancelreturn, rettv); + } + + xfree(xp_arg); + + // Since the user typed this, no need to wait for return. + need_wait_return = false; + msg_didout = false; + cmd_silent = cmd_silent_save; +} + +/// Builds a process argument vector from a VimL object (typval_T). +/// +/// @param[in] cmd_tv VimL object +/// @param[out] cmd Returns the command or executable name. +/// @param[out] executable Returns `false` if argv[0] is not executable. +/// +/// @return Result of `shell_build_argv()` if `cmd_tv` is a String. +/// Else, string values of `cmd_tv` copied to a (char **) list with +/// argv[0] resolved to full path ($PATHEXT-resolved on Windows). +char **tv_to_argv(typval_T *cmd_tv, const char **cmd, bool *executable) +{ + if (cmd_tv->v_type == VAR_STRING) { // String => "shell semantics". + const char *cmd_str = tv_get_string(cmd_tv); + if (cmd) { + *cmd = cmd_str; + } + return shell_build_argv(cmd_str, NULL); + } + + if (cmd_tv->v_type != VAR_LIST) { + semsg(_(e_invarg2), "expected String or List"); + return NULL; + } + + list_T *argl = cmd_tv->vval.v_list; + int argc = tv_list_len(argl); + if (!argc) { + emsg(_(e_invarg)); // List must have at least one item. + return NULL; + } + + const char *arg0 = tv_get_string_chk(TV_LIST_ITEM_TV(tv_list_first(argl))); + char *exe_resolved = NULL; + if (!arg0 || !os_can_exe(arg0, &exe_resolved, true)) { + if (arg0 && executable) { + char buf[IOSIZE]; + snprintf(buf, sizeof(buf), "'%s' is not executable", arg0); + semsg(_(e_invargNval), "cmd", buf); + *executable = false; + } + return NULL; + } + + if (cmd) { + *cmd = exe_resolved; + } + + // Build the argument vector + int i = 0; + char **argv = xcalloc((size_t)argc + 1, sizeof(char *)); + TV_LIST_ITER_CONST(argl, arg, { + const char *a = tv_get_string_chk(TV_LIST_ITEM_TV(arg)); + if (!a) { + // Did emsg in tv_get_string_chk; just deallocate argv. + shell_free_argv(argv); + xfree(exe_resolved); + return NULL; + } + argv[i++] = xstrdup(a); + }); + // Replace argv[0] with absolute path. The only reason for this is to make + // $PATHEXT work on Windows with jobstart([…]). #9569 + xfree(argv[0]); + argv[0] = exe_resolved; + + return argv; +} + +void return_register(int regname, typval_T *rettv) +{ + char buf[2] = { (char)regname, 0 }; + + rettv->v_type = VAR_STRING; + rettv->vval.v_string = xstrdup(buf); +} + +void screenchar_adjust(ScreenGrid **grid, int *row, int *col) +{ + // TODO(bfredl): this is a hack for legacy tests which use screenchar() + // to check printed messages on the screen (but not floats etc + // as these are not legacy features). If the compositor is refactored to + // have its own buffer, this should just read from it instead. + msg_scroll_flush(); + + *grid = ui_comp_get_grid_at_coord(*row, *col); + + // Make `row` and `col` relative to the grid + *row -= (*grid)->comp_row; + *col -= (*grid)->comp_col; +} + +/// Set line or list of lines in buffer "buf". +void set_buffer_lines(buf_T *buf, linenr_T lnum_arg, bool append, const typval_T *lines, + typval_T *rettv) + FUNC_ATTR_NONNULL_ARG(4, 5) +{ + linenr_T lnum = lnum_arg + (append ? 1 : 0); + const char *line = NULL; + list_T *l = NULL; + listitem_T *li = NULL; + long added = 0; + linenr_T append_lnum; + buf_T *curbuf_save = NULL; + win_T *curwin_save = NULL; + const bool is_curbuf = buf == curbuf; + const bool save_VIsual_active = VIsual_active; + + // When using the current buffer ml_mfp will be set if needed. Useful when + // setline() is used on startup. For other buffers the buffer must be + // loaded. + if (buf == NULL || (!is_curbuf && buf->b_ml.ml_mfp == NULL) || lnum < 1) { + rettv->vval.v_number = 1; // FAIL + return; + } + + if (!is_curbuf) { + VIsual_active = false; + curbuf_save = curbuf; + curwin_save = curwin; + curbuf = buf; + find_win_for_curbuf(); + } + + if (append) { + // appendbufline() uses the line number below which we insert + append_lnum = lnum - 1; + } else { + // setbufline() uses the line number above which we insert, we only + // append if it's below the last line + append_lnum = curbuf->b_ml.ml_line_count; + } + + if (lines->v_type == VAR_LIST) { + l = lines->vval.v_list; + li = tv_list_first(l); + } else { + line = tv_get_string_chk(lines); + } + + // Default result is zero == OK. + for (;;) { + if (lines->v_type == VAR_LIST) { + // List argument, get next string. + if (li == NULL) { + break; + } + line = tv_get_string_chk(TV_LIST_ITEM_TV(li)); + li = TV_LIST_ITEM_NEXT(l, li); + } + + rettv->vval.v_number = 1; // FAIL + if (line == NULL || lnum > curbuf->b_ml.ml_line_count + 1) { + break; + } + + // When coming here from Insert mode, sync undo, so that this can be + // undone separately from what was previously inserted. + if (u_sync_once == 2) { + u_sync_once = 1; // notify that u_sync() was called + u_sync(true); + } + + if (!append && lnum <= curbuf->b_ml.ml_line_count) { + // Existing line, replace it. + int old_len = (int)STRLEN(ml_get(lnum)); + if (u_savesub(lnum) == OK + && ml_replace(lnum, (char *)line, true) == OK) { + inserted_bytes(lnum, 0, old_len, (int)STRLEN(line)); + if (is_curbuf && lnum == curwin->w_cursor.lnum) { + check_cursor_col(); + } + rettv->vval.v_number = 0; // OK + } + } else if (added > 0 || u_save(lnum - 1, lnum) == OK) { + // append the line. + added++; + if (ml_append(lnum - 1, (char *)line, 0, false) == OK) { + rettv->vval.v_number = 0; // OK + } + } + + if (l == NULL) { // only one string argument + break; + } + lnum++; + } + + if (added > 0) { + appended_lines_mark(append_lnum, added); + + // Only adjust the cursor for buffers other than the current, unless it + // is the current window. For curbuf and other windows it has been done + // in mark_adjust_internal(). + FOR_ALL_TAB_WINDOWS(tp, wp) { + if (wp->w_buffer == buf + && (wp->w_buffer != curbuf || wp == curwin) + && wp->w_cursor.lnum > append_lnum) { + wp->w_cursor.lnum += (linenr_T)added; + } + } + check_cursor_col(); + update_topline(curwin); + } + + if (!is_curbuf) { + curbuf = curbuf_save; + curwin = curwin_save; + VIsual_active = save_VIsual_active; + } +} + +/// "stdpath()" helper for list results +void get_xdg_var_list(const XDGVarType xdg, typval_T *rettv) + FUNC_ATTR_NONNULL_ALL +{ + const void *iter = NULL; + list_T *const list = tv_list_alloc(kListLenShouldKnow); + rettv->v_type = VAR_LIST; + rettv->vval.v_list = list; + tv_list_ref(list); + char *const dirs = stdpaths_get_xdg_var(xdg); + if (dirs == NULL) { + return; + } + do { + size_t dir_len; + const char *dir; + iter = vim_env_iter(ENV_SEPCHAR, dirs, iter, &dir, &dir_len); + if (dir != NULL && dir_len > 0) { + char *dir_with_nvim = xmemdupz(dir, dir_len); + dir_with_nvim = concat_fnames_realloc(dir_with_nvim, "nvim", true); + tv_list_append_string(list, dir_with_nvim, (ssize_t)strlen(dir_with_nvim)); + xfree(dir_with_nvim); + } + } while (iter != NULL); + xfree(dirs); +} + +static list_T *string_to_list(const char *str, size_t len, const bool keepempty) +{ + if (!keepempty && str[len - 1] == NL) { + len--; + } + list_T *const list = tv_list_alloc(kListLenMayKnow); + encode_list_write(list, str, len); + return list; +} + +/// os_system wrapper. Handles 'verbose', :profile, and v:shell_error. +void get_system_output_as_rettv(typval_T *argvars, typval_T *rettv, bool retlist) +{ + proftime_T wait_time; + bool profiling = do_profiling == PROF_YES; + + rettv->v_type = VAR_STRING; + rettv->vval.v_string = NULL; + + if (check_secure()) { + return; + } + + // get input to the shell command (if any), and its length + ptrdiff_t input_len; + char *input = save_tv_as_string(&argvars[1], &input_len, false); + if (input_len < 0) { + assert(input == NULL); + return; + } + + // get shell command to execute + bool executable = true; + char **argv = tv_to_argv(&argvars[0], NULL, &executable); + if (!argv) { + if (!executable) { + set_vim_var_nr(VV_SHELL_ERROR, (long)-1); + } + xfree(input); + return; // Already did emsg. + } + + if (p_verbose > 3) { + char *cmdstr = shell_argv_to_str(argv); + verbose_enter_scroll(); + smsg(_("Executing command: \"%s\""), cmdstr); + msg_puts("\n\n"); + verbose_leave_scroll(); + xfree(cmdstr); + } + + if (profiling) { + prof_child_enter(&wait_time); + } + + // execute the command + size_t nread = 0; + char *res = NULL; + int status = os_system(argv, input, (size_t)input_len, &res, &nread); + + if (profiling) { + prof_child_exit(&wait_time); + } + + xfree(input); + + set_vim_var_nr(VV_SHELL_ERROR, (long)status); + + if (res == NULL) { + if (retlist) { + // return an empty list when there's no output + tv_list_alloc_ret(rettv, 0); + } else { + rettv->vval.v_string = xstrdup(""); + } + return; + } + + if (retlist) { + int keepempty = 0; + if (argvars[1].v_type != VAR_UNKNOWN && argvars[2].v_type != VAR_UNKNOWN) { + keepempty = (int)tv_get_number(&argvars[2]); + } + rettv->vval.v_list = string_to_list(res, nread, (bool)keepempty); + tv_list_ref(rettv->vval.v_list); + rettv->v_type = VAR_LIST; + + xfree(res); + } else { + // res may contain several NULs before the final terminating one. + // Replace them with SOH (1) like in get_cmd_output() to avoid truncation. + memchrsub(res, NUL, 1, nread); +#ifdef USE_CRNL + // translate <CR><NL> into <NL> + char *d = res; + for (char *s = res; *s; ++s) { + if (s[0] == CAR && s[1] == NL) { + ++s; + } + + *d++ = *s; + } + + *d = NUL; +#endif + rettv->vval.v_string = res; + } +} + +bool callback_from_typval(Callback *const callback, typval_T *const arg) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + int r = OK; + + if (arg->v_type == VAR_PARTIAL && arg->vval.v_partial != NULL) { + callback->data.partial = arg->vval.v_partial; + callback->data.partial->pt_refcount++; + callback->type = kCallbackPartial; + } else if (arg->v_type == VAR_STRING + && arg->vval.v_string != NULL + && ascii_isdigit(*arg->vval.v_string)) { + r = FAIL; + } else if (arg->v_type == VAR_FUNC || arg->v_type == VAR_STRING) { + char *name = arg->vval.v_string; + if (name == NULL) { + r = FAIL; + } else if (*name == NUL) { + callback->type = kCallbackNone; + callback->data.funcref = NULL; + } else { + func_ref((char_u *)name); + callback->data.funcref = xstrdup(name); + callback->type = kCallbackFuncref; + } + } else if (nlua_is_table_from_lua(arg)) { + // TODO(tjdvries): UnifiedCallback + char *name = (char *)nlua_register_table_as_callable(arg); + + if (name != NULL) { + callback->data.funcref = xstrdup(name); + callback->type = kCallbackFuncref; + } else { + r = FAIL; + } + } else if (arg->v_type == VAR_SPECIAL + || (arg->v_type == VAR_NUMBER && arg->vval.v_number == 0)) { + callback->type = kCallbackNone; + callback->data.funcref = NULL; + } else { + r = FAIL; + } + + if (r == FAIL) { + emsg(_("E921: Invalid callback argument")); + return false; + } + return true; +} + +bool callback_call(Callback *const callback, const int argcount_in, typval_T *const argvars_in, + typval_T *const rettv) + FUNC_ATTR_NONNULL_ALL +{ + partial_T *partial; + char *name; + Array args = ARRAY_DICT_INIT; + Object rv; + switch (callback->type) { + case kCallbackFuncref: + name = callback->data.funcref; + partial = NULL; + break; + + case kCallbackPartial: + partial = callback->data.partial; + name = partial_name(partial); + break; + + case kCallbackLua: + rv = nlua_call_ref(callback->data.luaref, NULL, args, true, NULL); + switch (rv.type) { + case kObjectTypeBoolean: + return rv.data.boolean; + default: + return false; + } + + case kCallbackNone: + return false; + break; + + default: + abort(); + } + + funcexe_T funcexe = FUNCEXE_INIT; + funcexe.firstline = curwin->w_cursor.lnum; + funcexe.lastline = curwin->w_cursor.lnum; + funcexe.evaluate = true; + funcexe.partial = partial; + return call_func(name, -1, rettv, argcount_in, argvars_in, &funcexe); +} + +static bool set_ref_in_callback(Callback *callback, int copyID, ht_stack_T **ht_stack, + list_stack_T **list_stack) +{ + typval_T tv; + switch (callback->type) { + case kCallbackFuncref: + case kCallbackNone: + break; + + case kCallbackPartial: + tv.v_type = VAR_PARTIAL; + tv.vval.v_partial = callback->data.partial; + return set_ref_in_item(&tv, copyID, ht_stack, list_stack); + break; + + default: + abort(); + } + return false; +} + +static bool set_ref_in_callback_reader(CallbackReader *reader, int copyID, ht_stack_T **ht_stack, + list_stack_T **list_stack) +{ + if (set_ref_in_callback(&reader->cb, copyID, ht_stack, list_stack)) { + return true; + } + + if (reader->self) { + typval_T tv; + tv.v_type = VAR_DICT; + tv.vval.v_dict = reader->self; + return set_ref_in_item(&tv, copyID, ht_stack, list_stack); + } + return false; +} + +timer_T *find_timer_by_nr(varnumber_T xx) +{ + return pmap_get(uint64_t)(&timers, (uint64_t)xx); +} + +void add_timer_info(typval_T *rettv, timer_T *timer) +{ + list_T *list = rettv->vval.v_list; + dict_T *dict = tv_dict_alloc(); + + tv_list_append_dict(list, dict); + tv_dict_add_nr(dict, S_LEN("id"), timer->timer_id); + tv_dict_add_nr(dict, S_LEN("time"), timer->timeout); + tv_dict_add_nr(dict, S_LEN("paused"), timer->paused); + + tv_dict_add_nr(dict, S_LEN("repeat"), + (timer->repeat_count < 0 ? -1 : timer->repeat_count)); + + dictitem_T *di = tv_dict_item_alloc("callback"); + if (tv_dict_add(dict, di) == FAIL) { + xfree(di); + return; + } + + callback_put(&timer->callback, &di->di_tv); +} + +void add_timer_info_all(typval_T *rettv) +{ + tv_list_alloc_ret(rettv, map_size(&timers)); + timer_T *timer; + map_foreach_value(&timers, timer, { + if (!timer->stopped) { + add_timer_info(rettv, timer); + } + }) +} + +/// invoked on the main loop +void timer_due_cb(TimeWatcher *tw, void *data) +{ + timer_T *timer = (timer_T *)data; + int save_did_emsg = did_emsg; + const int called_emsg_before = called_emsg; + const bool save_ex_pressedreturn = get_pressedreturn(); + + if (timer->stopped || timer->paused) { + return; + } + + timer->refcount++; + // if repeat was negative repeat forever + if (timer->repeat_count >= 0 && --timer->repeat_count == 0) { + timer_stop(timer); + } + + typval_T argv[2] = { TV_INITIAL_VALUE, TV_INITIAL_VALUE }; + argv[0].v_type = VAR_NUMBER; + argv[0].vval.v_number = timer->timer_id; + typval_T rettv = TV_INITIAL_VALUE; + + callback_call(&timer->callback, 1, argv, &rettv); + + // Handle error message + if (called_emsg > called_emsg_before && did_emsg) { + timer->emsg_count++; + if (current_exception != NULL) { + discard_current_exception(); + } + } + did_emsg = save_did_emsg; + set_pressedreturn(save_ex_pressedreturn); + + if (timer->emsg_count >= 3) { + timer_stop(timer); + } + + tv_clear(&rettv); + + if (!timer->stopped && timer->timeout == 0) { + // special case: timeout=0 means the callback will be + // invoked again on the next event loop tick. + // we don't use uv_idle_t to not spin the event loop + // when the main loop is blocked. + time_watcher_start(&timer->tw, timer_due_cb, 0, 0); + } + timer_decref(timer); +} + +uint64_t timer_start(const long timeout, const int repeat_count, const Callback *const callback) +{ + timer_T *timer = xmalloc(sizeof *timer); + timer->refcount = 1; + timer->stopped = false; + timer->paused = false; + timer->emsg_count = 0; + timer->repeat_count = repeat_count; + timer->timeout = timeout; + timer->timer_id = (int)last_timer_id++; + timer->callback = *callback; + + time_watcher_init(&main_loop, &timer->tw, timer); + timer->tw.events = multiqueue_new_child(main_loop.events); + // if main loop is blocked, don't queue up multiple events + timer->tw.blockable = true; + time_watcher_start(&timer->tw, timer_due_cb, (uint64_t)timeout, (uint64_t)timeout); + + pmap_put(uint64_t)(&timers, (uint64_t)timer->timer_id, timer); + return (uint64_t)timer->timer_id; +} + +void timer_stop(timer_T *timer) +{ + if (timer->stopped) { + // avoid double free + return; + } + timer->stopped = true; + time_watcher_stop(&timer->tw); + time_watcher_close(&timer->tw, timer_close_cb); +} + +/// This will be run on the main loop after the last timer_due_cb, so at this +/// point it is safe to free the callback. +static void timer_close_cb(TimeWatcher *tw, void *data) +{ + timer_T *timer = (timer_T *)data; + multiqueue_free(timer->tw.events); + callback_free(&timer->callback); + pmap_del(uint64_t)(&timers, (uint64_t)timer->timer_id); + timer_decref(timer); +} + +static void timer_decref(timer_T *timer) +{ + if (--timer->refcount == 0) { + xfree(timer); + } +} + +void timer_stop_all(void) +{ + timer_T *timer; + map_foreach_value(&timers, timer, { + timer_stop(timer); + }) +} + +void timer_teardown(void) +{ + timer_stop_all(); +} + +/// Write "list" of strings to file "fd". +/// +/// @param fp File to write to. +/// @param[in] list List to write. +/// @param[in] binary Whether to write in binary mode. +/// +/// @return true in case of success, false otherwise. +bool write_list(FileDescriptor *const fp, const list_T *const list, const bool binary) + FUNC_ATTR_NONNULL_ARG(1) +{ + int error = 0; + TV_LIST_ITER_CONST(list, li, { + const char *const s = tv_get_string_chk(TV_LIST_ITEM_TV(li)); + if (s == NULL) { + return false; + } + const char *hunk_start = s; + for (const char *p = hunk_start;; p++) { + if (*p == NUL || *p == NL) { + if (p != hunk_start) { + const ptrdiff_t written = file_write(fp, hunk_start, + (size_t)(p - hunk_start)); + if (written < 0) { + error = (int)written; + goto write_list_error; + } + } + if (*p == NUL) { + break; + } else { + hunk_start = p + 1; + const ptrdiff_t written = file_write(fp, (char[]){ NUL }, 1); + if (written < 0) { + error = (int)written; + break; + } + } + } + } + if (!binary || TV_LIST_ITEM_NEXT(list, li) != NULL) { + const ptrdiff_t written = file_write(fp, "\n", 1); + if (written < 0) { + error = (int)written; + goto write_list_error; + } + } + }); + if ((error = file_flush(fp)) != 0) { + goto write_list_error; + } + return true; +write_list_error: + semsg(_(e_write2), os_strerror(error)); + return false; +} + +/// Write a blob to file with descriptor `fp`. +/// +/// @param[in] fp File to write to. +/// @param[in] blob Blob to write. +/// +/// @return true on success, or false on failure. +bool write_blob(FileDescriptor *const fp, const blob_T *const blob) + FUNC_ATTR_NONNULL_ARG(1) +{ + int error = 0; + const int len = tv_blob_len(blob); + if (len > 0) { + const ptrdiff_t written = file_write(fp, blob->bv_ga.ga_data, (size_t)len); + if (written < (ptrdiff_t)len) { + error = (int)written; + goto write_blob_error; + } + } + error = file_flush(fp); + if (error != 0) { + goto write_blob_error; + } + return true; +write_blob_error: + semsg(_(e_write2), os_strerror(error)); + return false; +} + +/// Read a blob from a file `fd`. +/// +/// @param[in] fd File to read from. +/// @param[in,out] blob Blob to write to. +/// +/// @return true on success, or false on failure. +bool read_blob(FILE *const fd, blob_T *const blob) + FUNC_ATTR_NONNULL_ALL +{ + FileInfo file_info; + if (!os_fileinfo_fd(fileno(fd), &file_info)) { + return false; + } + const int size = (int)os_fileinfo_size(&file_info); + ga_grow(&blob->bv_ga, size); + blob->bv_ga.ga_len = size; + if (fread(blob->bv_ga.ga_data, 1, (size_t)blob->bv_ga.ga_len, fd) + < (size_t)blob->bv_ga.ga_len) { + return false; + } + return true; +} + +/// Saves a typval_T as a string. +/// +/// For lists or buffers, replaces NLs with NUL and separates items with NLs. +/// +/// @param[in] tv Value to store as a string. +/// @param[out] len Length of the resulting string or -1 on error. +/// @param[in] endnl If true, the output will end in a newline (if a list). +/// @returns an allocated string if `tv` represents a VimL string, list, or +/// number; NULL otherwise. +char *save_tv_as_string(typval_T *tv, ptrdiff_t *const len, bool endnl) + FUNC_ATTR_MALLOC FUNC_ATTR_NONNULL_ALL +{ + *len = 0; + if (tv->v_type == VAR_UNKNOWN) { + return NULL; + } + + // For other types, let tv_get_string_buf_chk() get the value or + // print an error. + if (tv->v_type != VAR_LIST && tv->v_type != VAR_NUMBER) { + const char *ret = tv_get_string_chk(tv); + if (ret) { + *len = (ptrdiff_t)strlen(ret); + return xmemdupz(ret, (size_t)(*len)); + } else { + *len = -1; + return NULL; + } + } + + if (tv->v_type == VAR_NUMBER) { // Treat number as a buffer-id. + buf_T *buf = buflist_findnr((int)tv->vval.v_number); + if (buf) { + for (linenr_T lnum = 1; lnum <= buf->b_ml.ml_line_count; lnum++) { + for (char *p = (char *)ml_get_buf(buf, lnum, false); *p != NUL; p++) { + *len += 1; + } + *len += 1; + } + } else { + semsg(_(e_nobufnr), tv->vval.v_number); + *len = -1; + return NULL; + } + + if (*len == 0) { + return NULL; + } + + char *ret = xmalloc((size_t)(*len) + 1); + char *end = ret; + for (linenr_T lnum = 1; lnum <= buf->b_ml.ml_line_count; lnum++) { + for (char *p = (char *)ml_get_buf(buf, lnum, false); *p != NUL; p++) { + *end++ = (*p == '\n') ? NUL : *p; + } + *end++ = '\n'; + } + *end = NUL; + *len = end - ret; + return ret; + } + + assert(tv->v_type == VAR_LIST); + // Pre-calculate the resulting length. + list_T *list = tv->vval.v_list; + TV_LIST_ITER_CONST(list, li, { + *len += (ptrdiff_t)strlen(tv_get_string(TV_LIST_ITEM_TV(li))) + 1; + }); + + if (*len == 0) { + return NULL; + } + + char *ret = xmalloc((size_t)(*len) + endnl); + char *end = ret; + TV_LIST_ITER_CONST(list, li, { + for (const char *s = tv_get_string(TV_LIST_ITEM_TV(li)); *s != NUL; s++) { + *end++ = (*s == '\n') ? NUL : *s; + } + if (endnl || TV_LIST_ITEM_NEXT(list, li) != NULL) { + *end++ = '\n'; + } + }); + *end = NUL; + *len = end - ret; + return ret; +} + +/// Convert the specified byte index of line 'lnum' in buffer 'buf' to a +/// character index. Works only for loaded buffers. Returns -1 on failure. +/// The index of the first byte and the first character is zero. +int buf_byteidx_to_charidx(buf_T *buf, linenr_T lnum, int byteidx) +{ + if (buf == NULL || buf->b_ml.ml_mfp == NULL) { + return -1; + } + + if (lnum > buf->b_ml.ml_line_count) { + lnum = buf->b_ml.ml_line_count; + } + + char *str = (char *)ml_get_buf(buf, lnum, false); + + if (*str == NUL) { + return 0; + } + + // count the number of characters + char *t = str; + int count; + for (count = 0; *t != NUL && t <= str + byteidx; count++) { + t += utfc_ptr2len(t); + } + + // In insert mode, when the cursor is at the end of a non-empty line, + // byteidx points to the NUL character immediately past the end of the + // string. In this case, add one to the character count. + if (*t == NUL && byteidx != 0 && t == str + byteidx) { + count++; + } + + return count - 1; +} + +/// Convert the specified character index of line 'lnum' in buffer 'buf' to a +/// byte index. Works only for loaded buffers. +/// The index of the first byte and the first character is zero. +/// +/// @return -1 on failure. +int buf_charidx_to_byteidx(buf_T *buf, linenr_T lnum, int charidx) +{ + if (buf == NULL || buf->b_ml.ml_mfp == NULL) { + return -1; + } + + if (lnum > buf->b_ml.ml_line_count) { + lnum = buf->b_ml.ml_line_count; + } + + char *str = (char *)ml_get_buf(buf, lnum, false); + + // Convert the character offset to a byte offset + char *t = str; + while (*t != NUL && --charidx > 0) { + t += utfc_ptr2len(t); + } + + return (int)(t - str); +} + +/// Translate a VimL object into a position +/// +/// Accepts VAR_LIST and VAR_STRING objects. Does not give an error for invalid +/// type. +/// +/// @param[in] tv Object to translate. +/// @param[in] dollar_lnum True when "$" is last line. +/// @param[out] ret_fnum Set to fnum for marks. +/// @param[in] charcol True to return character column. +/// +/// @return Pointer to position or NULL in case of error (e.g. invalid type). +pos_T *var2fpos(const typval_T *const tv, const bool dollar_lnum, int *const ret_fnum, + const bool charcol) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL +{ + static pos_T pos; + + // Argument can be [lnum, col, coladd]. + if (tv->v_type == VAR_LIST) { + list_T *l; + int len; + bool error = false; + listitem_T *li; + + l = tv->vval.v_list; + if (l == NULL) { + return NULL; + } + + // Get the line number. + pos.lnum = (linenr_T)tv_list_find_nr(l, 0L, &error); + if (error || pos.lnum <= 0 || pos.lnum > curbuf->b_ml.ml_line_count) { + // Invalid line number. + return NULL; + } + + // Get the column number. + pos.col = (colnr_T)tv_list_find_nr(l, 1L, &error); + if (error) { + return NULL; + } + if (charcol) { + len = mb_charlen(ml_get(pos.lnum)); + } else { + len = (int)STRLEN(ml_get(pos.lnum)); + } + + // We accept "$" for the column number: last column. + li = tv_list_find(l, 1L); + if (li != NULL && TV_LIST_ITEM_TV(li)->v_type == VAR_STRING + && TV_LIST_ITEM_TV(li)->vval.v_string != NULL + && STRCMP(TV_LIST_ITEM_TV(li)->vval.v_string, "$") == 0) { + pos.col = len + 1; + } + + // Accept a position up to the NUL after the line. + if (pos.col == 0 || (int)pos.col > len + 1) { + // Invalid column number. + return NULL; + } + pos.col--; + + // Get the virtual offset. Defaults to zero. + pos.coladd = (colnr_T)tv_list_find_nr(l, 2L, &error); + if (error) { + pos.coladd = 0; + } + + return &pos; + } + + const char *const name = tv_get_string_chk(tv); + if (name == NULL) { + return NULL; + } + + pos.lnum = 0; + if (name[0] == '.') { + // cursor + pos = curwin->w_cursor; + } else if (name[0] == 'v' && name[1] == NUL) { + // Visual start + if (VIsual_active) { + pos = VIsual; + } else { + pos = curwin->w_cursor; + } + } else if (name[0] == '\'') { + // mark + int mname = (uint8_t)name[1]; + const fmark_T *const fm = mark_get(curbuf, curwin, NULL, kMarkAll, mname); + if (fm == NULL || fm->mark.lnum <= 0) { + return NULL; + } + pos = fm->mark; + // Vimscript behavior, only provide fnum if mark is global. + *ret_fnum = ASCII_ISUPPER(mname) || ascii_isdigit(mname) ? fm->fnum: *ret_fnum; + } + if (pos.lnum != 0) { + if (charcol) { + pos.col = buf_byteidx_to_charidx(curbuf, pos.lnum, pos.col); + } + return &pos; + } + + pos.coladd = 0; + + if (name[0] == 'w' && dollar_lnum) { + pos.col = 0; + if (name[1] == '0') { // "w0": first visible line + update_topline(curwin); + // In silent Ex mode topline is zero, but that's not a valid line + // number; use one instead. + pos.lnum = curwin->w_topline > 0 ? curwin->w_topline : 1; + return &pos; + } else if (name[1] == '$') { // "w$": last visible line + validate_botline(curwin); + // In silent Ex mode botline is zero, return zero then. + pos.lnum = curwin->w_botline > 0 ? curwin->w_botline - 1 : 0; + return &pos; + } + } else if (name[0] == '$') { // last column or line + if (dollar_lnum) { + pos.lnum = curbuf->b_ml.ml_line_count; + pos.col = 0; + } else { + pos.lnum = curwin->w_cursor.lnum; + if (charcol) { + pos.col = (colnr_T)mb_charlen(get_cursor_line_ptr()); + } else { + pos.col = (colnr_T)STRLEN(get_cursor_line_ptr()); + } + } + return &pos; + } + return NULL; +} + +/// Convert list in "arg" into a position and optional file number. +/// When "fnump" is NULL there is no file number, only 3 items. +/// Note that the column is passed on as-is, the caller may want to decrement +/// it to use 1 for the first column. +/// +/// @return FAIL when conversion is not possible, doesn't check the position for +/// validity. +int list2fpos(typval_T *arg, pos_T *posp, int *fnump, colnr_T *curswantp, bool charcol) +{ + list_T *l; + int i = 0; + long n; + + // List must be: [fnum, lnum, col, coladd, curswant], where "fnum" is only + // there when "fnump" isn't NULL; "coladd" and "curswant" are optional. + if (arg->v_type != VAR_LIST + || (l = arg->vval.v_list) == NULL + || tv_list_len(l) < (fnump == NULL ? 2 : 3) + || tv_list_len(l) > (fnump == NULL ? 4 : 5)) { + return FAIL; + } + + if (fnump != NULL) { + n = tv_list_find_nr(l, i++, NULL); // fnum + if (n < 0) { + return FAIL; + } + if (n == 0) { + n = curbuf->b_fnum; // Current buffer. + } + *fnump = (int)n; + } + + n = tv_list_find_nr(l, i++, NULL); // lnum + if (n < 0) { + return FAIL; + } + posp->lnum = (linenr_T)n; + + n = tv_list_find_nr(l, i++, NULL); // col + if (n < 0) { + return FAIL; + } + // If character position is specified, then convert to byte position + if (charcol) { + // Get the text for the specified line in a loaded buffer + buf_T *buf = buflist_findnr(fnump == NULL ? curbuf->b_fnum : *fnump); + if (buf == NULL || buf->b_ml.ml_mfp == NULL) { + return FAIL; + } + n = buf_charidx_to_byteidx(buf, posp->lnum, (int)n) + 1; + } + posp->col = (colnr_T)n; + + n = tv_list_find_nr(l, i, NULL); // off + if (n < 0) { + posp->coladd = 0; + } else { + posp->coladd = (colnr_T)n; + } + + if (curswantp != NULL) { + *curswantp = (colnr_T)tv_list_find_nr(l, i + 1, NULL); // curswant + } + + return OK; +} + +/// Get the length of an environment variable name. +/// Advance "arg" to the first character after the name. +/// +/// @return 0 for error. +int get_env_len(const char **arg) +{ + int len; + + const char *p; + for (p = *arg; vim_isIDc(*p); p++) {} + if (p == *arg) { // No name found. + return 0; + } + + len = (int)(p - *arg); + *arg = p; + return len; +} + +/// Get the length of the name of a function or internal variable. +/// +/// @param arg is advanced to the first non-white character after the name. +/// +/// @return 0 if something is wrong. +int get_id_len(const char **const arg) +{ + int len; + + // Find the end of the name. + const char *p; + for (p = *arg; eval_isnamec(*p); p++) { + if (*p == ':') { + // "s:" is start of "s:var", but "n:" is not and can be used in + // slice "[n:]". Also "xx:" is not a namespace. + len = (int)(p - *arg); + if (len > 1 + || (len == 1 && vim_strchr(namespace_char, **arg) == NULL)) { + break; + } + } + } + if (p == *arg) { // no name found + return 0; + } + + len = (int)(p - *arg); + *arg = (const char *)skipwhite(p); + + return len; +} + +/// Get the length of the name of a variable or function. +/// Only the name is recognized, does not handle ".key" or "[idx]". +/// +/// @param arg is advanced to the first non-white character after the name. +/// If the name contains 'magic' {}'s, expand them and return the +/// expanded name in an allocated string via 'alias' - caller must free. +/// +/// @return -1 if curly braces expansion failed or +/// 0 if something else is wrong. +int get_name_len(const char **const arg, char **alias, bool evaluate, bool verbose) +{ + int len; + + *alias = NULL; // default to no alias + + if ((*arg)[0] == (char)K_SPECIAL && (*arg)[1] == (char)KS_EXTRA + && (*arg)[2] == (char)KE_SNR) { + // Hard coded <SNR>, already translated. + *arg += 3; + return get_id_len(arg) + 3; + } + len = eval_fname_script(*arg); + if (len > 0) { + // literal "<SID>", "s:" or "<SNR>" + *arg += len; + } + + // Find the end of the name; check for {} construction. + char *expr_start; + char *expr_end; + const char *p = find_name_end((*arg), (const char **)&expr_start, (const char **)&expr_end, + len > 0 ? 0 : FNE_CHECK_START); + if (expr_start != NULL) { + if (!evaluate) { + len += (int)(p - *arg); + *arg = (const char *)skipwhite(p); + return len; + } + + /* + * Include any <SID> etc in the expanded string: + * Thus the -len here. + */ + char *temp_string = make_expanded_name(*arg - len, expr_start, expr_end, (char *)p); + if (temp_string == NULL) { + return -1; + } + *alias = temp_string; + *arg = (const char *)skipwhite(p); + return (int)STRLEN(temp_string); + } + + len += get_id_len(arg); + // Only give an error when there is something, otherwise it will be + // reported at a higher level. + if (len == 0 && verbose && **arg != NUL) { + semsg(_(e_invexpr2), *arg); + } + + return len; +} + +/// Find the end of a variable or function name, taking care of magic braces. +/// +/// @param expr_start if not NULL, then `expr_start` and `expr_end` are set to the +/// start and end of the first magic braces item. +/// +/// @param flags can have FNE_INCL_BR and FNE_CHECK_START. +/// +/// @return a pointer to just after the name. Equal to "arg" if there is no +/// valid name. +const char *find_name_end(const char *arg, const char **expr_start, const char **expr_end, + int flags) +{ + int mb_nest = 0; + int br_nest = 0; + int len; + + if (expr_start != NULL) { + *expr_start = NULL; + *expr_end = NULL; + } + + // Quick check for valid starting character. + if ((flags & FNE_CHECK_START) && !eval_isnamec1(*arg) && *arg != '{') { + return arg; + } + + const char *p; + for (p = arg; *p != NUL + && (eval_isnamec(*p) + || *p == '{' + || ((flags & FNE_INCL_BR) && (*p == '[' || *p == '.')) + || mb_nest != 0 + || br_nest != 0); MB_PTR_ADV(p)) { + if (*p == '\'') { + // skip over 'string' to avoid counting [ and ] inside it. + for (p = p + 1; *p != NUL && *p != '\''; MB_PTR_ADV(p)) {} + if (*p == NUL) { + break; + } + } else if (*p == '"') { + // skip over "str\"ing" to avoid counting [ and ] inside it. + for (p = p + 1; *p != NUL && *p != '"'; MB_PTR_ADV(p)) { + if (*p == '\\' && p[1] != NUL) { + ++p; + } + } + if (*p == NUL) { + break; + } + } else if (br_nest == 0 && mb_nest == 0 && *p == ':') { + // "s:" is start of "s:var", but "n:" is not and can be used in + // slice "[n:]". Also "xx:" is not a namespace. But {ns}: is. + len = (int)(p - arg); + if ((len > 1 && p[-1] != '}') + || (len == 1 && vim_strchr(namespace_char, *arg) == NULL)) { + break; + } + } + + if (mb_nest == 0) { + if (*p == '[') { + ++br_nest; + } else if (*p == ']') { + --br_nest; + } + } + + if (br_nest == 0) { + if (*p == '{') { + mb_nest++; + if (expr_start != NULL && *expr_start == NULL) { + *expr_start = p; + } + } else if (*p == '}') { + mb_nest--; + if (expr_start != NULL && mb_nest == 0 && *expr_end == NULL) { + *expr_end = p; + } + } + } + } + + return p; +} + +/// Expands out the 'magic' {}'s in a variable/function name. +/// Note that this can call itself recursively, to deal with +/// constructs like foo{bar}{baz}{bam} +/// The four pointer arguments point to "foo{expre}ss{ion}bar" +/// "in_start" ^ +/// "expr_start" ^ +/// "expr_end" ^ +/// "in_end" ^ +/// +/// @return a new allocated string, which the caller must free or +/// NULL for failure. +static char *make_expanded_name(const char *in_start, char *expr_start, char *expr_end, + char *in_end) +{ + char c1; + char *retval = NULL; + char *temp_result; + char *nextcmd = NULL; + + if (expr_end == NULL || in_end == NULL) { + return NULL; + } + *expr_start = NUL; + *expr_end = NUL; + c1 = *in_end; + *in_end = NUL; + + temp_result = eval_to_string(expr_start + 1, &nextcmd, false); + if (temp_result != NULL && nextcmd == NULL) { + retval = xmalloc(STRLEN(temp_result) + (size_t)(expr_start - in_start) + + (size_t)(in_end - expr_end) + 1); + STRCPY(retval, in_start); + STRCAT(retval, temp_result); + STRCAT(retval, expr_end + 1); + } + xfree(temp_result); + + *in_end = c1; // put char back for error messages + *expr_start = '{'; + *expr_end = '}'; + + if (retval != NULL) { + temp_result = (char *)find_name_end(retval, + (const char **)&expr_start, + (const char **)&expr_end, 0); + if (expr_start != NULL) { + // Further expansion! + temp_result = make_expanded_name(retval, expr_start, + expr_end, temp_result); + xfree(retval); + retval = temp_result; + } + } + + return retval; +} + +/// @return TRUE if character "c" can be used in a variable or function name. +/// Does not include '{' or '}' for magic braces. +int eval_isnamec(int c) +{ + return ASCII_ISALNUM(c) || c == '_' || c == ':' || c == AUTOLOAD_CHAR; +} + +/// @return TRUE if character "c" can be used as the first character in a +/// variable or function name (excluding '{' and '}'). +int eval_isnamec1(int c) +{ + return ASCII_ISALPHA(c) || c == '_'; +} + +/// Get typval_T v: variable value. +typval_T *get_vim_var_tv(int idx) +{ + return &vimvars[idx].vv_tv; +} + +/// Get number v: variable value. +varnumber_T get_vim_var_nr(int idx) FUNC_ATTR_PURE +{ + return vimvars[idx].vv_nr; +} + +/// Get string v: variable value. Uses a static buffer, can only be used once. +/// If the String variable has never been set, return an empty string. +/// Never returns NULL. +char *get_vim_var_str(int idx) + FUNC_ATTR_PURE FUNC_ATTR_NONNULL_RET +{ + return (char *)tv_get_string(&vimvars[idx].vv_tv); +} + +/// Get List v: variable value. Caller must take care of reference count when +/// needed. +list_T *get_vim_var_list(int idx) FUNC_ATTR_PURE +{ + return vimvars[idx].vv_list; +} + +/// Get Dictionary v: variable value. Caller must take care of reference count +/// when needed. +dict_T *get_vim_var_dict(int idx) FUNC_ATTR_PURE +{ + return vimvars[idx].vv_dict; +} + +/// Set v:char to character "c". +void set_vim_var_char(int c) +{ + char buf[MB_MAXBYTES + 1]; + + buf[utf_char2bytes(c, buf)] = NUL; + set_vim_var_string(VV_CHAR, buf, -1); +} + +/// Set v:count to "count" and v:count1 to "count1". +/// +/// @param set_prevcount if TRUE, first set v:prevcount from v:count. +void set_vcount(long count, long count1, int set_prevcount) +{ + if (set_prevcount) { + vimvars[VV_PREVCOUNT].vv_nr = vimvars[VV_COUNT].vv_nr; + } + vimvars[VV_COUNT].vv_nr = count; + vimvars[VV_COUNT1].vv_nr = count1; +} + +/// Set number v: variable to the given value +/// +/// @param[in] idx Index of variable to set. +/// @param[in] val Value to set to. +void set_vim_var_nr(const VimVarIndex idx, const varnumber_T val) +{ + tv_clear(&vimvars[idx].vv_tv); + vimvars[idx].vv_type = VAR_NUMBER; + vimvars[idx].vv_nr = val; +} + +/// Set boolean v: {true, false} to the given value +/// +/// @param[in] idx Index of variable to set. +/// @param[in] val Value to set to. +void set_vim_var_bool(const VimVarIndex idx, const BoolVarValue val) +{ + tv_clear(&vimvars[idx].vv_tv); + vimvars[idx].vv_type = VAR_BOOL; + vimvars[idx].vv_bool = val; +} + +/// Set special v: variable to the given value +/// +/// @param[in] idx Index of variable to set. +/// @param[in] val Value to set to. +void set_vim_var_special(const VimVarIndex idx, const SpecialVarValue val) +{ + tv_clear(&vimvars[idx].vv_tv); + vimvars[idx].vv_type = VAR_SPECIAL; + vimvars[idx].vv_special = val; +} + +/// Set string v: variable to the given string +/// +/// @param[in] idx Index of variable to set. +/// @param[in] val Value to set to. Will be copied. +/// @param[in] len Length of that value or -1 in which case strlen() will be +/// used. +void set_vim_var_string(const VimVarIndex idx, const char *const val, const ptrdiff_t len) +{ + tv_clear(&vimvars[idx].vv_di.di_tv); + vimvars[idx].vv_type = VAR_STRING; + if (val == NULL) { + vimvars[idx].vv_str = NULL; + } else if (len == -1) { + vimvars[idx].vv_str = xstrdup(val); + } else { + vimvars[idx].vv_str = xstrndup(val, (size_t)len); + } +} + +/// Set list v: variable to the given list +/// +/// @param[in] idx Index of variable to set. +/// @param[in,out] val Value to set to. Reference count will be incremented. +void set_vim_var_list(const VimVarIndex idx, list_T *const val) +{ + tv_clear(&vimvars[idx].vv_di.di_tv); + vimvars[idx].vv_type = VAR_LIST; + vimvars[idx].vv_list = val; + if (val != NULL) { + tv_list_ref(val); + } +} + +/// Set Dictionary v: variable to the given dictionary +/// +/// @param[in] idx Index of variable to set. +/// @param[in,out] val Value to set to. Reference count will be incremented. +/// Also keys of the dictionary will be made read-only. +void set_vim_var_dict(const VimVarIndex idx, dict_T *const val) +{ + tv_clear(&vimvars[idx].vv_di.di_tv); + vimvars[idx].vv_type = VAR_DICT; + vimvars[idx].vv_dict = val; + + if (val != NULL) { + val->dv_refcount++; + // Set readonly + tv_dict_set_keys_readonly(val); + } +} + +/// Set the v:argv list. +void set_argv_var(char **argv, int argc) +{ + list_T *l = tv_list_alloc(argc); + int i; + + tv_list_set_lock(l, VAR_FIXED); + for (i = 0; i < argc; i++) { + tv_list_append_string(l, (const char *const)argv[i], -1); + TV_LIST_ITEM_TV(tv_list_last(l))->v_lock = VAR_FIXED; + } + set_vim_var_list(VV_ARGV, l); +} + +/// Set v:register if needed. +void set_reg_var(int c) +{ + char regname; + + if (c == 0 || c == ' ') { + regname = '"'; + } else { + regname = (char)c; + } + // Avoid free/alloc when the value is already right. + if (vimvars[VV_REG].vv_str == NULL || vimvars[VV_REG].vv_str[0] != c) { + set_vim_var_string(VV_REG, ®name, 1); + } +} + +/// Get or set v:exception. If "oldval" == NULL, return the current value. +/// Otherwise, restore the value to "oldval" and return NULL. +/// Must always be called in pairs to save and restore v:exception! Does not +/// take care of memory allocations. +char *v_exception(char *oldval) +{ + if (oldval == NULL) { + return vimvars[VV_EXCEPTION].vv_str; + } + + vimvars[VV_EXCEPTION].vv_str = oldval; + return NULL; +} + +/// Get or set v:throwpoint. If "oldval" == NULL, return the current value. +/// Otherwise, restore the value to "oldval" and return NULL. +/// Must always be called in pairs to save and restore v:throwpoint! Does not +/// take care of memory allocations. +char *v_throwpoint(char *oldval) +{ + if (oldval == NULL) { + return vimvars[VV_THROWPOINT].vv_str; + } + + vimvars[VV_THROWPOINT].vv_str = oldval; + return NULL; +} + +/// Set v:cmdarg. +/// If "eap" != NULL, use "eap" to generate the value and return the old value. +/// If "oldarg" != NULL, restore the value to "oldarg" and return NULL. +/// Must always be called in pairs! +char *set_cmdarg(exarg_T *eap, char *oldarg) +{ + char *oldval = vimvars[VV_CMDARG].vv_str; + if (eap == NULL) { + xfree(oldval); + vimvars[VV_CMDARG].vv_str = oldarg; + return NULL; + } + + size_t len = 0; + if (eap->force_bin == FORCE_BIN) { + len = 6; + } else if (eap->force_bin == FORCE_NOBIN) { + len = 8; + } + + if (eap->read_edit) { + len += 7; + } + + if (eap->force_ff != 0) { + len += 10; // " ++ff=unix" + } + if (eap->force_enc != 0) { + len += STRLEN(eap->cmd + eap->force_enc) + 7; + } + if (eap->bad_char != 0) { + len += 7 + 4; // " ++bad=" + "keep" or "drop" + } + + const size_t newval_len = len + 1; + char *newval = xmalloc(newval_len); + + if (eap->force_bin == FORCE_BIN) { + snprintf(newval, newval_len, " ++bin"); + } else if (eap->force_bin == FORCE_NOBIN) { + snprintf(newval, newval_len, " ++nobin"); + } else { + *newval = NUL; + } + + if (eap->read_edit) { + STRCAT(newval, " ++edit"); + } + + if (eap->force_ff != 0) { + snprintf(newval + STRLEN(newval), newval_len, " ++ff=%s", + eap->force_ff == 'u' ? "unix" : + eap->force_ff == 'd' ? "dos" : "mac"); + } + if (eap->force_enc != 0) { + snprintf(newval + STRLEN(newval), newval_len, " ++enc=%s", + eap->cmd + eap->force_enc); + } + if (eap->bad_char == BAD_KEEP) { + STRCPY(newval + STRLEN(newval), " ++bad=keep"); + } else if (eap->bad_char == BAD_DROP) { + STRCPY(newval + STRLEN(newval), " ++bad=drop"); + } else if (eap->bad_char != 0) { + snprintf(newval + STRLEN(newval), newval_len, " ++bad=%c", + eap->bad_char); + } + vimvars[VV_CMDARG].vv_str = newval; + return oldval; +} + +/// Check if variable "name[len]" is a local variable or an argument. +/// If so, "*eval_lavars_used" is set to true. +static void check_vars(const char *name, size_t len) +{ + if (eval_lavars_used == NULL) { + return; + } + + const char *varname; + hashtab_T *ht = find_var_ht(name, len, &varname); + + if (ht == get_funccal_local_ht() || ht == get_funccal_args_ht()) { + if (find_var(name, len, NULL, true) != NULL) { + *eval_lavars_used = true; + } + } +} + +/// check if special v:lua value for calling lua functions +bool is_luafunc(partial_T *partial) + FUNC_ATTR_PURE +{ + return partial == vvlua_partial; +} + +/// check if special v:lua value for calling lua functions +static bool tv_is_luafunc(typval_T *tv) +{ + return tv->v_type == VAR_PARTIAL && is_luafunc(tv->vval.v_partial); +} + +/// Skips one character past the end of the name of a v:lua function. +/// @param p Pointer to the char AFTER the "v:lua." prefix. +/// @return Pointer to the char one past the end of the function's name. +const char *skip_luafunc_name(const char *p) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT +{ + while (ASCII_ISALNUM(*p) || *p == '_' || *p == '-' || *p == '.' || *p == '\'') { + p++; + } + return p; +} + +/// check the function name after "v:lua." +int check_luafunc_name(const char *const str, const bool paren) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT +{ + const char *const p = skip_luafunc_name(str); + if (*p != (paren ? '(' : NUL)) { + return 0; + } else { + return (int)(p - str); + } +} + +/// Handle: +/// - expr[expr], expr[expr:expr] subscript +/// - ".name" lookup +/// - function call with Funcref variable: func(expr) +/// - method call: var->method() +/// +/// Can all be combined in any order: dict.func(expr)[idx]['func'](expr)->len() +/// +/// @param evaluate do more than finding the end +/// @param verbose give error messages +/// @param start_leader start of '!' and '-' prefixes +/// @param end_leaderp end of '!' and '-' prefixes +int handle_subscript(const char **const arg, typval_T *rettv, int evaluate, int verbose, + const char *const start_leader, const char **const end_leaderp) +{ + int ret = OK; + dict_T *selfdict = NULL; + const char *lua_funcname = NULL; + + if (tv_is_luafunc(rettv)) { + if (**arg != '.') { + tv_clear(rettv); + ret = FAIL; + } else { + (*arg)++; + + lua_funcname = *arg; + const int len = check_luafunc_name(*arg, true); + if (len == 0) { + tv_clear(rettv); + ret = FAIL; + } + (*arg) += len; + } + } + + // "." is ".name" lookup when we found a dict. + while (ret == OK + && (((**arg == '[' || (**arg == '.' && rettv->v_type == VAR_DICT) + || (**arg == '(' && (!evaluate || tv_is_func(*rettv)))) + && !ascii_iswhite(*(*arg - 1))) + || (**arg == '-' && (*arg)[1] == '>'))) { + if (**arg == '(') { + ret = call_func_rettv((char **)arg, rettv, evaluate, selfdict, NULL, lua_funcname); + + // Stop the expression evaluation when immediately aborting on + // error, or when an interrupt occurred or an exception was thrown + // but not caught. + if (aborting()) { + if (ret == OK) { + tv_clear(rettv); + } + ret = FAIL; + } + tv_dict_unref(selfdict); + selfdict = NULL; + } else if (**arg == '-') { + // Expression "-1.0->method()" applies the leader "-" before + // applying ->. + if (evaluate && *end_leaderp > start_leader) { + ret = eval7_leader(rettv, (char *)start_leader, end_leaderp); + } + if (ret == OK) { + if ((*arg)[2] == '{') { + // expr->{lambda}() + ret = eval_lambda((char **)arg, rettv, evaluate, verbose); + } else { + // expr->name() + ret = eval_method((char **)arg, rettv, evaluate, verbose); + } + } + } else { // **arg == '[' || **arg == '.' + tv_dict_unref(selfdict); + if (rettv->v_type == VAR_DICT) { + selfdict = rettv->vval.v_dict; + if (selfdict != NULL) { + ++selfdict->dv_refcount; + } + } else { + selfdict = NULL; + } + if (eval_index((char **)arg, rettv, evaluate, verbose) == FAIL) { + tv_clear(rettv); + ret = FAIL; + } + } + } + + // Turn "dict.Func" into a partial for "Func" bound to "dict". + if (selfdict != NULL && tv_is_func(*rettv)) { + set_selfdict(rettv, selfdict); + } + + tv_dict_unref(selfdict); + return ret; +} + +void set_selfdict(typval_T *const rettv, dict_T *const selfdict) +{ + // Don't do this when "dict.Func" is already a partial that was bound + // explicitly (pt_auto is false). + if (rettv->v_type == VAR_PARTIAL && !rettv->vval.v_partial->pt_auto + && rettv->vval.v_partial->pt_dict != NULL) { + return; + } + make_partial(selfdict, rettv); +} + +/// Find variable "name" in the list of variables. +/// Careful: "a:0" variables don't have a name. +/// When "htp" is not NULL we are writing to the variable, set "htp" to the +/// hashtab_T used. +/// +/// @return a pointer to it if found, NULL if not found. +dictitem_T *find_var(const char *const name, const size_t name_len, hashtab_T **htp, + int no_autoload) +{ + const char *varname; + hashtab_T *const ht = find_var_ht(name, name_len, &varname); + if (htp != NULL) { + *htp = ht; + } + if (ht == NULL) { + return NULL; + } + dictitem_T *const ret = find_var_in_ht(ht, *name, + varname, + name_len - (size_t)(varname - name), + no_autoload || htp != NULL); + if (ret != NULL) { + return ret; + } + + // Search in parent scope for lambda + return find_var_in_scoped_ht(name, name_len, no_autoload || htp != NULL); +} + +/// Find variable in hashtab. +/// When "varname" is empty returns curwin/curtab/etc vars dictionary. +/// +/// @param[in] ht Hashtab to find variable in. +/// @param[in] htname Hashtab name (first character). +/// @param[in] varname Variable name. +/// @param[in] varname_len Variable name length. +/// @param[in] no_autoload If true then autoload scripts will not be sourced +/// if autoload variable was not found. +/// +/// @return pointer to the dictionary item with the found variable or NULL if it +/// was not found. +dictitem_T *find_var_in_ht(hashtab_T *const ht, int htname, const char *const varname, + const size_t varname_len, int no_autoload) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL +{ + hashitem_T *hi; + + if (varname_len == 0) { + // Must be something like "s:", otherwise "ht" would be NULL. + switch (htname) { + case 's': + return (dictitem_T *)&SCRIPT_SV(current_sctx.sc_sid)->sv_var; + case 'g': + return (dictitem_T *)&globvars_var; + case 'v': + return (dictitem_T *)&vimvars_var; + case 'b': + return (dictitem_T *)&curbuf->b_bufvar; + case 'w': + return (dictitem_T *)&curwin->w_winvar; + case 't': + return (dictitem_T *)&curtab->tp_winvar; + case 'l': + return get_funccal_local_var(); + case 'a': + return get_funccal_args_var(); + } + return NULL; + } + + hi = hash_find_len(ht, varname, varname_len); + if (HASHITEM_EMPTY(hi)) { + // For global variables we may try auto-loading the script. If it + // worked find the variable again. Don't auto-load a script if it was + // loaded already, otherwise it would be loaded every time when + // checking if a function name is a Funcref variable. + if (ht == &globvarht && !no_autoload) { + // Note: script_autoload() may make "hi" invalid. It must either + // be obtained again or not used. + if (!script_autoload(varname, varname_len, false) || aborting()) { + return NULL; + } + hi = hash_find_len(ht, varname, varname_len); + } + if (HASHITEM_EMPTY(hi)) { + return NULL; + } + } + return TV_DICT_HI2DI(hi); +} + +/// Finds the dict (g:, l:, s:, …) and hashtable used for a variable. +/// +/// Assigns SID if s: scope is accessed from Lua or anonymous Vimscript. #15994 +/// +/// @param[in] name Variable name, possibly with scope prefix. +/// @param[in] name_len Variable name length. +/// @param[out] varname Will be set to the start of the name without scope +/// prefix. +/// @param[out] d Scope dictionary. +/// +/// @return Scope hashtab, NULL if name is not valid. +hashtab_T *find_var_ht_dict(const char *name, const size_t name_len, const char **varname, + dict_T **d) +{ + hashitem_T *hi; + funccall_T *funccal = get_funccal(); + *d = NULL; + + if (name_len == 0) { + return NULL; + } + if (name_len == 1 || name[1] != ':') { + // name has implicit scope + if (name[0] == ':' || name[0] == AUTOLOAD_CHAR) { + // The name must not start with a colon or #. + return NULL; + } + *varname = name; + + // "version" is "v:version" in all scopes + hi = hash_find_len(&compat_hashtab, name, name_len); + if (!HASHITEM_EMPTY(hi)) { + return &compat_hashtab; + } + + if (funccal == NULL) { // global variable + *d = &globvardict; + } else { // l: variable + *d = &funccal->l_vars; + } + goto end; + } + + *varname = name + 2; + if (*name == 'g') { // global variable + *d = &globvardict; + } else if (name_len > 2 + && (memchr(name + 2, ':', name_len - 2) != NULL + || memchr(name + 2, AUTOLOAD_CHAR, name_len - 2) != NULL)) { + // There must be no ':' or '#' in the rest of the name if g: was not used + return NULL; + } + + if (*name == 'b') { // buffer variable + *d = curbuf->b_vars; + } else if (*name == 'w') { // window variable + *d = curwin->w_vars; + } else if (*name == 't') { // tab page variable + *d = curtab->tp_vars; + } else if (*name == 'v') { // v: variable + *d = &vimvardict; + } else if (*name == 'a' && funccal != NULL) { // function argument + *d = &funccal->l_avars; + } else if (*name == 'l' && funccal != NULL) { // local variable + *d = &funccal->l_vars; + } else if (*name == 's' // script variable + && (current_sctx.sc_sid > 0 || current_sctx.sc_sid == SID_STR + || current_sctx.sc_sid == SID_LUA) + && current_sctx.sc_sid <= ga_scripts.ga_len) { + // For anonymous scripts without a script item, create one now so script vars can be used + if (current_sctx.sc_sid == SID_LUA) { + // try to resolve lua filename & line no so it can be shown in lastset messages. + nlua_set_sctx(¤t_sctx); + if (current_sctx.sc_sid != SID_LUA) { + // Great we have valid location. Now here this out we'll create a new + // script context with the name and lineno of this one. why ? + // for behavioral consistency. With this different anonymous exec from + // same file can't access each others script local stuff. We need to do + // this all other cases except this will act like that otherwise. + const LastSet last_set = (LastSet){ + .script_ctx = current_sctx, + .channel_id = LUA_INTERNAL_CALL, + }; + bool should_free; + // should_free is ignored as script_sctx will be resolved to a fnmae + // & new_script_item will consume it. + char *sc_name = (char *)get_scriptname(last_set, &should_free); + new_script_item(sc_name, ¤t_sctx.sc_sid); + } + } + if (current_sctx.sc_sid == SID_STR || current_sctx.sc_sid == SID_LUA) { + // Create SID if s: scope is accessed from Lua or anon Vimscript. #15994 + new_script_item(NULL, ¤t_sctx.sc_sid); + } + *d = &SCRIPT_SV(current_sctx.sc_sid)->sv_dict; + } + +end: + return *d ? &(*d)->dv_hashtab : NULL; +} + +/// Find the hashtable used for a variable +/// +/// @param[in] name Variable name, possibly with scope prefix. +/// @param[in] name_len Variable name length. +/// @param[out] varname Will be set to the start of the name without scope +/// prefix. +/// +/// @return Scope hashtab, NULL if name is not valid. +hashtab_T *find_var_ht(const char *name, const size_t name_len, const char **varname) +{ + dict_T *d; + return find_var_ht_dict(name, name_len, varname, &d); +} + +/// Allocate a new hashtab for a sourced script. It will be used while +/// sourcing this script and when executing functions defined in the script. +void new_script_vars(scid_T id) +{ + hashtab_T *ht; + scriptvar_T *sv; + + ga_grow(&ga_scripts, id - ga_scripts.ga_len); + { + /* Re-allocating ga_data means that an ht_array pointing to + * ht_smallarray becomes invalid. We can recognize this: ht_mask is + * at its init value. Also reset "v_dict", it's always the same. */ + for (int i = 1; i <= ga_scripts.ga_len; ++i) { + ht = &SCRIPT_VARS(i); + if (ht->ht_mask == HT_INIT_SIZE - 1) { + ht->ht_array = ht->ht_smallarray; + } + sv = SCRIPT_SV(i); + sv->sv_var.di_tv.vval.v_dict = &sv->sv_dict; + } + + while (ga_scripts.ga_len < id) { + sv = SCRIPT_SV(ga_scripts.ga_len + 1) = xcalloc(1, sizeof(scriptvar_T)); + init_var_dict(&sv->sv_dict, &sv->sv_var, VAR_SCOPE); + ++ga_scripts.ga_len; + } + } +} + +/// Initialize dictionary "dict" as a scope and set variable "dict_var" to +/// point to it. +void init_var_dict(dict_T *dict, ScopeDictDictItem *dict_var, ScopeType scope) +{ + hash_init(&dict->dv_hashtab); + dict->dv_lock = VAR_UNLOCKED; + dict->dv_scope = scope; + dict->dv_refcount = DO_NOT_FREE_CNT; + dict->dv_copyID = 0; + dict_var->di_tv.vval.v_dict = dict; + dict_var->di_tv.v_type = VAR_DICT; + dict_var->di_tv.v_lock = VAR_FIXED; + dict_var->di_flags = DI_FLAGS_RO | DI_FLAGS_FIX; + dict_var->di_key[0] = NUL; + QUEUE_INIT(&dict->watchers); +} + +/// Unreference a dictionary initialized by init_var_dict(). +void unref_var_dict(dict_T *dict) +{ + /* Now the dict needs to be freed if no one else is using it, go back to + * normal reference counting. */ + dict->dv_refcount -= DO_NOT_FREE_CNT - 1; + tv_dict_unref(dict); +} + +/// Make a copy of an item +/// +/// Lists and Dictionaries are also copied. +/// +/// @param[in] conv If not NULL, convert all copied strings. +/// @param[in] from Value to copy. +/// @param[out] to Location where to copy to. +/// @param[in] deep If true, use copy the container and all of the contained +/// containers (nested). +/// @param[in] copyID If non-zero then when container is referenced more then +/// once then copy of it that was already done is used. E.g. +/// when copying list `list = [list2, list2]` (`list[0] is +/// list[1]`) var_item_copy with zero copyID will emit +/// a copy with (`copy[0] isnot copy[1]`), with non-zero it +/// will emit a copy with (`copy[0] is copy[1]`) like in the +/// original list. Not used when deep is false. +int var_item_copy(const vimconv_T *const conv, typval_T *const from, typval_T *const to, + const bool deep, const int copyID) + FUNC_ATTR_NONNULL_ARG(2, 3) +{ + static int recurse = 0; + int ret = OK; + + if (recurse >= DICT_MAXNEST) { + emsg(_("E698: variable nested too deep for making a copy")); + return FAIL; + } + ++recurse; + + switch (from->v_type) { + case VAR_NUMBER: + case VAR_FLOAT: + case VAR_FUNC: + case VAR_PARTIAL: + case VAR_BOOL: + case VAR_SPECIAL: + tv_copy(from, to); + break; + case VAR_STRING: + if (conv == NULL || conv->vc_type == CONV_NONE + || from->vval.v_string == NULL) { + tv_copy(from, to); + } else { + to->v_type = VAR_STRING; + to->v_lock = VAR_UNLOCKED; + if ((to->vval.v_string = (char *)string_convert((vimconv_T *)conv, + (char_u *)from->vval.v_string, + NULL)) + == NULL) { + to->vval.v_string = xstrdup(from->vval.v_string); + } + } + break; + case VAR_LIST: + to->v_type = VAR_LIST; + to->v_lock = VAR_UNLOCKED; + if (from->vval.v_list == NULL) { + to->vval.v_list = NULL; + } else if (copyID != 0 && tv_list_copyid(from->vval.v_list) == copyID) { + // Use the copy made earlier. + to->vval.v_list = tv_list_latest_copy(from->vval.v_list); + tv_list_ref(to->vval.v_list); + } else { + to->vval.v_list = tv_list_copy(conv, from->vval.v_list, deep, copyID); + } + if (to->vval.v_list == NULL && from->vval.v_list != NULL) { + ret = FAIL; + } + break; + case VAR_BLOB: + tv_blob_copy(from, to); + break; + case VAR_DICT: + to->v_type = VAR_DICT; + to->v_lock = VAR_UNLOCKED; + if (from->vval.v_dict == NULL) { + to->vval.v_dict = NULL; + } else if (copyID != 0 && from->vval.v_dict->dv_copyID == copyID) { + // use the copy made earlier + to->vval.v_dict = from->vval.v_dict->dv_copydict; + ++to->vval.v_dict->dv_refcount; + } else { + to->vval.v_dict = tv_dict_copy(conv, from->vval.v_dict, deep, copyID); + } + if (to->vval.v_dict == NULL && from->vval.v_dict != NULL) { + ret = FAIL; + } + break; + case VAR_UNKNOWN: + internal_error("var_item_copy(UNKNOWN)"); + ret = FAIL; + } + --recurse; + return ret; +} + +/// ":echo expr1 ..." print each argument separated with a space, add a +/// newline at the end. +/// ":echon expr1 ..." print each argument plain. +void ex_echo(exarg_T *eap) +{ + char *arg = eap->arg; + typval_T rettv; + bool atstart = true; + bool need_clear = true; + const int did_emsg_before = did_emsg; + const int called_emsg_before = called_emsg; + + if (eap->skip) { + ++emsg_skip; + } + while (*arg != NUL && *arg != '|' && *arg != '\n' && !got_int) { + // If eval1() causes an error message the text from the command may + // still need to be cleared. E.g., "echo 22,44". + need_clr_eos = true; + + { + char *p = arg; + if (eval1(&arg, &rettv, !eap->skip) == FAIL) { + // Report the invalid expression unless the expression evaluation + // has been cancelled due to an aborting error, an interrupt, or an + // exception. + if (!aborting() && did_emsg == did_emsg_before + && called_emsg == called_emsg_before) { + semsg(_(e_invexpr2), p); + } + need_clr_eos = false; + break; + } + need_clr_eos = false; + } + + if (!eap->skip) { + if (atstart) { + atstart = false; + // Call msg_start() after eval1(), evaluating the expression + // may cause a message to appear. + if (eap->cmdidx == CMD_echo) { + // Mark the saved text as finishing the line, so that what + // follows is displayed on a new line when scrolling back + // at the more prompt. + msg_sb_eol(); + msg_start(); + } + } else if (eap->cmdidx == CMD_echo) { + msg_puts_attr(" ", echo_attr); + } + char *tofree = encode_tv2echo(&rettv, NULL); + if (*tofree != NUL) { + msg_ext_set_kind("echo"); + msg_multiline_attr(tofree, echo_attr, true, &need_clear); + } + xfree(tofree); + } + tv_clear(&rettv); + arg = skipwhite(arg); + } + eap->nextcmd = (char *)check_nextcmd((char_u *)arg); + + if (eap->skip) { + emsg_skip--; + } else { + // remove text that may still be there from the command + if (need_clear) { + msg_clr_eos(); + } + if (eap->cmdidx == CMD_echo) { + msg_end(); + } + } +} + +/// ":echohl {name}". +void ex_echohl(exarg_T *eap) +{ + echo_attr = syn_name2attr((char_u *)eap->arg); +} + +/// ":execute expr1 ..." execute the result of an expression. +/// ":echomsg expr1 ..." Print a message +/// ":echoerr expr1 ..." Print an error +/// Each gets spaces around each argument and a newline at the end for +/// echo commands +void ex_execute(exarg_T *eap) +{ + char *arg = eap->arg; + typval_T rettv; + int ret = OK; + garray_T ga; + int save_did_emsg; + + ga_init(&ga, 1, 80); + + if (eap->skip) { + ++emsg_skip; + } + while (*arg != NUL && *arg != '|' && *arg != '\n') { + ret = eval1_emsg(&arg, &rettv, !eap->skip); + if (ret == FAIL) { + break; + } + + if (!eap->skip) { + const char *const argstr = eap->cmdidx == CMD_execute + ? tv_get_string(&rettv) + : rettv.v_type == VAR_STRING + ? encode_tv2echo(&rettv, NULL) + : encode_tv2string(&rettv, NULL); + const size_t len = strlen(argstr); + ga_grow(&ga, (int)len + 2); + if (!GA_EMPTY(&ga)) { + ((char_u *)(ga.ga_data))[ga.ga_len++] = ' '; + } + memcpy((char_u *)(ga.ga_data) + ga.ga_len, argstr, len + 1); + if (eap->cmdidx != CMD_execute) { + xfree((void *)argstr); + } + ga.ga_len += (int)len; + } + + tv_clear(&rettv); + arg = skipwhite(arg); + } + + if (ret != FAIL && ga.ga_data != NULL) { + if (eap->cmdidx == CMD_echomsg || eap->cmdidx == CMD_echoerr) { + // Mark the already saved text as finishing the line, so that what + // follows is displayed on a new line when scrolling back at the + // more prompt. + msg_sb_eol(); + } + + if (eap->cmdidx == CMD_echomsg) { + msg_ext_set_kind("echomsg"); + msg_attr(ga.ga_data, echo_attr); + ui_flush(); + } else if (eap->cmdidx == CMD_echoerr) { + // We don't want to abort following commands, restore did_emsg. + save_did_emsg = did_emsg; + msg_ext_set_kind("echoerr"); + emsg(ga.ga_data); + if (!force_abort) { + did_emsg = save_did_emsg; + } + } else if (eap->cmdidx == CMD_execute) { + do_cmdline(ga.ga_data, eap->getline, eap->cookie, DOCMD_NOWAIT|DOCMD_VERBOSE); + } + } + + ga_clear(&ga); + + if (eap->skip) { + --emsg_skip; + } + + eap->nextcmd = (char *)check_nextcmd((char_u *)arg); +} + +/// Skip over the name of an option: "&option", "&g:option" or "&l:option". +/// +/// @param arg points to the "&" or '+' when called, to "option" when returning. +/// +/// @return NULL when no option name found. Otherwise pointer to the char +/// after the option name. +const char *find_option_end(const char **const arg, int *const opt_flags) +{ + const char *p = *arg; + + ++p; + if (*p == 'g' && p[1] == ':') { + *opt_flags = OPT_GLOBAL; + p += 2; + } else if (*p == 'l' && p[1] == ':') { + *opt_flags = OPT_LOCAL; + p += 2; + } else { + *opt_flags = 0; + } + + if (!ASCII_ISALPHA(*p)) { + return NULL; + } + *arg = p; + + if (p[0] == 't' && p[1] == '_' && p[2] != NUL && p[3] != NUL) { + p += 4; // t_xx/termcap option + } else { + while (ASCII_ISALPHA(*p)) { + p++; + } + } + return p; +} + +/// Start profiling function "fp". +void func_do_profile(ufunc_T *fp) +{ + int len = fp->uf_lines.ga_len; + + if (!fp->uf_prof_initialized) { + if (len == 0) { + len = 1; // avoid getting error for allocating zero bytes + } + fp->uf_tm_count = 0; + fp->uf_tm_self = profile_zero(); + fp->uf_tm_total = profile_zero(); + + if (fp->uf_tml_count == NULL) { + fp->uf_tml_count = xcalloc((size_t)len, sizeof(int)); + } + + if (fp->uf_tml_total == NULL) { + fp->uf_tml_total = xcalloc((size_t)len, sizeof(proftime_T)); + } + + if (fp->uf_tml_self == NULL) { + fp->uf_tml_self = xcalloc((size_t)len, sizeof(proftime_T)); + } + + fp->uf_tml_idx = -1; + fp->uf_prof_initialized = true; + } + + fp->uf_profiling = TRUE; +} + +/// Dump the profiling results for all functions in file "fd". +void func_dump_profile(FILE *fd) +{ + hashitem_T *hi; + int todo; + ufunc_T *fp; + ufunc_T **sorttab; + int st_len = 0; + + todo = (int)func_hashtab.ht_used; + if (todo == 0) { + return; // nothing to dump + } + + sorttab = xmalloc(sizeof(ufunc_T *) * (size_t)todo); + + for (hi = func_hashtab.ht_array; todo > 0; ++hi) { + if (!HASHITEM_EMPTY(hi)) { + --todo; + fp = HI2UF(hi); + if (fp->uf_prof_initialized) { + sorttab[st_len++] = fp; + + if (fp->uf_name[0] == K_SPECIAL) { + fprintf(fd, "FUNCTION <SNR>%s()\n", fp->uf_name + 3); + } else { + fprintf(fd, "FUNCTION %s()\n", fp->uf_name); + } + if (fp->uf_script_ctx.sc_sid != 0) { + bool should_free; + const LastSet last_set = (LastSet){ + .script_ctx = fp->uf_script_ctx, + .channel_id = 0, + }; + char *p = (char *)get_scriptname(last_set, &should_free); + fprintf(fd, " Defined: %s:%" PRIdLINENR "\n", + p, fp->uf_script_ctx.sc_lnum); + if (should_free) { + xfree(p); + } + } + if (fp->uf_tm_count == 1) { + fprintf(fd, "Called 1 time\n"); + } else { + fprintf(fd, "Called %d times\n", fp->uf_tm_count); + } + fprintf(fd, "Total time: %s\n", profile_msg(fp->uf_tm_total)); + fprintf(fd, " Self time: %s\n", profile_msg(fp->uf_tm_self)); + fprintf(fd, "\n"); + fprintf(fd, "count total (s) self (s)\n"); + + for (int i = 0; i < fp->uf_lines.ga_len; ++i) { + if (FUNCLINE(fp, i) == NULL) { + continue; + } + prof_func_line(fd, fp->uf_tml_count[i], + &fp->uf_tml_total[i], &fp->uf_tml_self[i], TRUE); + fprintf(fd, "%s\n", FUNCLINE(fp, i)); + } + fprintf(fd, "\n"); + } + } + } + + if (st_len > 0) { + qsort((void *)sorttab, (size_t)st_len, sizeof(ufunc_T *), + prof_total_cmp); + prof_sort_list(fd, sorttab, st_len, "TOTAL", FALSE); + qsort((void *)sorttab, (size_t)st_len, sizeof(ufunc_T *), + prof_self_cmp); + prof_sort_list(fd, sorttab, st_len, "SELF", TRUE); + } + + xfree(sorttab); +} + +/// @param prefer_self when equal print only self time +static void prof_sort_list(FILE *fd, ufunc_T **sorttab, int st_len, char *title, int prefer_self) +{ + int i; + ufunc_T *fp; + + fprintf(fd, "FUNCTIONS SORTED ON %s TIME\n", title); + fprintf(fd, "count total (s) self (s) function\n"); + for (i = 0; i < 20 && i < st_len; ++i) { + fp = sorttab[i]; + prof_func_line(fd, fp->uf_tm_count, &fp->uf_tm_total, &fp->uf_tm_self, + prefer_self); + if (fp->uf_name[0] == K_SPECIAL) { + fprintf(fd, " <SNR>%s()\n", fp->uf_name + 3); + } else { + fprintf(fd, " %s()\n", fp->uf_name); + } + } + fprintf(fd, "\n"); +} + +/// Print the count and times for one function or function line. +/// +/// @param prefer_self when equal print only self time +static void prof_func_line(FILE *fd, int count, proftime_T *total, proftime_T *self, + int prefer_self) +{ + if (count > 0) { + fprintf(fd, "%5d ", count); + if (prefer_self && profile_equal(*total, *self)) { + fprintf(fd, " "); + } else { + fprintf(fd, "%s ", profile_msg(*total)); + } + if (!prefer_self && profile_equal(*total, *self)) { + fprintf(fd, " "); + } else { + fprintf(fd, "%s ", profile_msg(*self)); + } + } else { + fprintf(fd, " "); + } +} + +/// Compare function for total time sorting. +static int prof_total_cmp(const void *s1, const void *s2) +{ + ufunc_T *p1 = *(ufunc_T **)s1; + ufunc_T *p2 = *(ufunc_T **)s2; + return profile_cmp(p1->uf_tm_total, p2->uf_tm_total); +} + +/// Compare function for self time sorting. +static int prof_self_cmp(const void *s1, const void *s2) +{ + ufunc_T *p1 = *(ufunc_T **)s1; + ufunc_T *p2 = *(ufunc_T **)s2; + return profile_cmp(p1->uf_tm_self, p2->uf_tm_self); +} + +/// Return the autoload script name for a function or variable name +/// Caller must make sure that "name" contains AUTOLOAD_CHAR. +/// +/// @param[in] name Variable/function name. +/// @param[in] name_len Name length. +/// +/// @return [allocated] autoload script name. +char *autoload_name(const char *const name, const size_t name_len) + FUNC_ATTR_MALLOC FUNC_ATTR_WARN_UNUSED_RESULT +{ + // Get the script file name: replace '#' with '/', append ".vim". + char *const scriptname = xmalloc(name_len + sizeof("autoload/.vim")); + memcpy(scriptname, "autoload/", sizeof("autoload/") - 1); + memcpy(scriptname + sizeof("autoload/") - 1, name, name_len); + size_t auchar_idx = 0; + for (size_t i = sizeof("autoload/") - 1; + i - sizeof("autoload/") + 1 < name_len; + i++) { + if (scriptname[i] == AUTOLOAD_CHAR) { + scriptname[i] = '/'; + auchar_idx = i; + } + } + memcpy(scriptname + auchar_idx, ".vim", sizeof(".vim")); + + return scriptname; +} + +/// If name has a package name try autoloading the script for it +/// +/// @param[in] name Variable/function name. +/// @param[in] name_len Name length. +/// @param[in] reload If true, load script again when already loaded. +/// +/// @return true if a package was loaded. +bool script_autoload(const char *const name, const size_t name_len, const bool reload) +{ + // If there is no '#' after name[0] there is no package name. + const char *p = memchr(name, AUTOLOAD_CHAR, name_len); + if (p == NULL || p == name) { + return false; + } + + bool ret = false; + char *tofree = autoload_name(name, name_len); + char *scriptname = tofree; + + // Find the name in the list of previously loaded package names. Skip + // "autoload/", it's always the same. + int i = 0; + for (; i < ga_loaded.ga_len; i++) { + if (STRCMP(((char **)ga_loaded.ga_data)[i] + 9, scriptname + 9) == 0) { + break; + } + } + if (!reload && i < ga_loaded.ga_len) { + ret = false; // Was loaded already. + } else { + // Remember the name if it wasn't loaded already. + if (i == ga_loaded.ga_len) { + GA_APPEND(char *, &ga_loaded, scriptname); + tofree = NULL; + } + + // Try loading the package from $VIMRUNTIME/autoload/<name>.vim + if (source_runtime(scriptname, 0) == OK) { + ret = true; + } + } + + xfree(tofree); + return ret; +} + +/// Called when starting to read a function line. +/// "sourcing_lnum" must be correct! +/// When skipping lines it may not actually be executed, but we won't find out +/// until later and we need to store the time now. +void func_line_start(void *cookie) +{ + funccall_T *fcp = (funccall_T *)cookie; + ufunc_T *fp = fcp->func; + + if (fp->uf_profiling && sourcing_lnum >= 1 + && sourcing_lnum <= fp->uf_lines.ga_len) { + fp->uf_tml_idx = sourcing_lnum - 1; + // Skip continuation lines. + while (fp->uf_tml_idx > 0 && FUNCLINE(fp, fp->uf_tml_idx) == NULL) { + fp->uf_tml_idx--; + } + fp->uf_tml_execed = false; + fp->uf_tml_start = profile_start(); + fp->uf_tml_children = profile_zero(); + fp->uf_tml_wait = profile_get_wait(); + } +} + +/// Called when actually executing a function line. +void func_line_exec(void *cookie) +{ + funccall_T *fcp = (funccall_T *)cookie; + ufunc_T *fp = fcp->func; + + if (fp->uf_profiling && fp->uf_tml_idx >= 0) { + fp->uf_tml_execed = TRUE; + } +} + +/// Called when done with a function line. +void func_line_end(void *cookie) +{ + funccall_T *fcp = (funccall_T *)cookie; + ufunc_T *fp = fcp->func; + + if (fp->uf_profiling && fp->uf_tml_idx >= 0) { + if (fp->uf_tml_execed) { + ++fp->uf_tml_count[fp->uf_tml_idx]; + fp->uf_tml_start = profile_end(fp->uf_tml_start); + fp->uf_tml_start = profile_sub_wait(fp->uf_tml_wait, fp->uf_tml_start); + fp->uf_tml_total[fp->uf_tml_idx] = + profile_add(fp->uf_tml_total[fp->uf_tml_idx], fp->uf_tml_start); + fp->uf_tml_self[fp->uf_tml_idx] = + profile_self(fp->uf_tml_self[fp->uf_tml_idx], fp->uf_tml_start, + fp->uf_tml_children); + } + fp->uf_tml_idx = -1; + } +} + +static var_flavour_T var_flavour(char *varname) + FUNC_ATTR_PURE +{ + char *p = varname; + + if (ASCII_ISUPPER(*p)) { + while (*(++p)) { + if (ASCII_ISLOWER(*p)) { + return VAR_FLAVOUR_SESSION; + } + } + return VAR_FLAVOUR_SHADA; + } else { + return VAR_FLAVOUR_DEFAULT; + } +} + +/// Iterate over global variables +/// +/// @warning No modifications to global variable dictionary must be performed +/// while iteration is in progress. +/// +/// @param[in] iter Iterator. Pass NULL to start iteration. +/// @param[out] name Variable name. +/// @param[out] rettv Variable value. +/// +/// @return Pointer that needs to be passed to next `var_shada_iter` invocation +/// or NULL to indicate that iteration is over. +const void *var_shada_iter(const void *const iter, const char **const name, typval_T *rettv, + var_flavour_T flavour) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ARG(2, 3) +{ + const hashitem_T *hi; + const hashitem_T *hifirst = globvarht.ht_array; + const size_t hinum = (size_t)globvarht.ht_mask + 1; + *name = NULL; + if (iter == NULL) { + hi = globvarht.ht_array; + while ((size_t)(hi - hifirst) < hinum + && (HASHITEM_EMPTY(hi) + || !(var_flavour((char *)hi->hi_key) & flavour))) { + hi++; + } + if ((size_t)(hi - hifirst) == hinum) { + return NULL; + } + } else { + hi = (const hashitem_T *)iter; + } + *name = (char *)TV_DICT_HI2DI(hi)->di_key; + tv_copy(&TV_DICT_HI2DI(hi)->di_tv, rettv); + while ((size_t)(++hi - hifirst) < hinum) { + if (!HASHITEM_EMPTY(hi) && (var_flavour((char *)hi->hi_key) & flavour)) { + return hi; + } + } + return NULL; +} + +void var_set_global(const char *const name, typval_T vartv) +{ + funccal_entry_T funccall_entry; + + save_funccal(&funccall_entry); + set_var(name, strlen(name), &vartv, false); + restore_funccal(); +} + +int store_session_globals(FILE *fd) +{ + TV_DICT_ITER(&globvardict, this_var, { + if ((this_var->di_tv.v_type == VAR_NUMBER + || this_var->di_tv.v_type == VAR_STRING) + && var_flavour((char *)this_var->di_key) == VAR_FLAVOUR_SESSION) { + // Escape special characters with a backslash. Turn a LF and + // CR into \n and \r. + char *const p = (char *)vim_strsave_escaped((const char_u *)tv_get_string(&this_var->di_tv), + (const char_u *)"\\\"\n\r"); + for (char *t = p; *t != NUL; t++) { + if (*t == '\n') { + *t = 'n'; + } else if (*t == '\r') { + *t = 'r'; + } + } + if ((fprintf(fd, "let %s = %c%s%c", + this_var->di_key, + ((this_var->di_tv.v_type == VAR_STRING) ? '"' + : ' '), + p, + ((this_var->di_tv.v_type == VAR_STRING) ? '"' + : ' ')) < 0) + || put_eol(fd) == FAIL) { + xfree(p); + return FAIL; + } + xfree(p); + } else if (this_var->di_tv.v_type == VAR_FLOAT + && var_flavour((char *)this_var->di_key) == VAR_FLAVOUR_SESSION) { + float_T f = this_var->di_tv.vval.v_float; + int sign = ' '; + + if (f < 0) { + f = -f; + sign = '-'; + } + if ((fprintf(fd, "let %s = %c%f", this_var->di_key, sign, f) < 0) + || put_eol(fd) == FAIL) { + return FAIL; + } + } + }); + return OK; +} + +/// Display script name where an item was last set. +/// Should only be invoked when 'verbose' is non-zero. +void last_set_msg(sctx_T script_ctx) +{ + const LastSet last_set = (LastSet){ + .script_ctx = script_ctx, + .channel_id = 0, + }; + option_last_set_msg(last_set); +} + +/// Displays where an option was last set. +/// +/// Should only be invoked when 'verbose' is non-zero. +void option_last_set_msg(LastSet last_set) +{ + if (last_set.script_ctx.sc_sid != 0) { + bool should_free; + char *p = (char *)get_scriptname(last_set, &should_free); + verbose_enter(); + msg_puts(_("\n\tLast set from ")); + msg_puts(p); + if (last_set.script_ctx.sc_lnum > 0) { + msg_puts(_(line_msg)); + msg_outnum((long)last_set.script_ctx.sc_lnum); + } + if (should_free) { + xfree(p); + } + verbose_leave(); + } +} + +// reset v:option_new, v:option_old, v:option_oldlocal, v:option_oldglobal, +// v:option_type, and v:option_command. +void reset_v_option_vars(void) +{ + set_vim_var_string(VV_OPTION_NEW, NULL, -1); + set_vim_var_string(VV_OPTION_OLD, NULL, -1); + set_vim_var_string(VV_OPTION_OLDLOCAL, NULL, -1); + set_vim_var_string(VV_OPTION_OLDGLOBAL, NULL, -1); + set_vim_var_string(VV_OPTION_COMMAND, NULL, -1); + set_vim_var_string(VV_OPTION_TYPE, NULL, -1); +} + +/// Adjust a filename, according to a string of modifiers. +/// *fnamep must be NUL terminated when called. When returning, the length is +/// determined by *fnamelen. +/// Returns VALID_ flags or -1 for failure. +/// When there is an error, *fnamep is set to NULL. +/// +/// @param src string with modifiers +/// @param tilde_file "~" is a file name, not $HOME +/// @param usedlen characters after src that are used +/// @param fnamep file name so far +/// @param bufp buffer for allocated file name or NULL +/// @param fnamelen length of fnamep +int modify_fname(char *src, bool tilde_file, size_t *usedlen, char **fnamep, char **bufp, + size_t *fnamelen) +{ + int valid = 0; + char *tail; + char *s, *p, *pbuf; + char dirname[MAXPATHL]; + int c; + bool has_fullname = false; + bool has_homerelative = false; + +repeat: + // ":p" - full path/file_name + if (src[*usedlen] == ':' && src[*usedlen + 1] == 'p') { + has_fullname = true; + + valid |= VALID_PATH; + *usedlen += 2; + + // Expand "~/path" for all systems and "~user/path" for Unix + if ((*fnamep)[0] == '~' +#if !defined(UNIX) + && ((*fnamep)[1] == '/' +# ifdef BACKSLASH_IN_FILENAME + || (*fnamep)[1] == '\\' +# endif + || (*fnamep)[1] == NUL) +#endif + && !(tilde_file && (*fnamep)[1] == NUL)) { + *fnamep = expand_env_save(*fnamep); + xfree(*bufp); // free any allocated file name + *bufp = *fnamep; + if (*fnamep == NULL) { + return -1; + } + } + + // When "/." or "/.." is used: force expansion to get rid of it. + for (p = *fnamep; *p != NUL; MB_PTR_ADV(p)) { + if (vim_ispathsep(*p) + && p[1] == '.' + && (p[2] == NUL + || vim_ispathsep(p[2]) + || (p[2] == '.' + && (p[3] == NUL || vim_ispathsep(p[3]))))) { + break; + } + } + + // FullName_save() is slow, don't use it when not needed. + if (*p != NUL || !vim_isAbsName((char_u *)(*fnamep))) { + *fnamep = FullName_save((*fnamep), *p != NUL); + xfree(*bufp); // free any allocated file name + *bufp = *fnamep; + if (*fnamep == NULL) { + return -1; + } + } + + // Append a path separator to a directory. + if (os_isdir((char_u *)(*fnamep))) { + // Make room for one or two extra characters. + *fnamep = xstrnsave(*fnamep, STRLEN(*fnamep) + 2); + xfree(*bufp); // free any allocated file name + *bufp = *fnamep; + add_pathsep(*fnamep); + } + } + + // ":." - path relative to the current directory + // ":~" - path relative to the home directory + // ":8" - shortname path - postponed till after + while (src[*usedlen] == ':' + && ((c = (char_u)src[*usedlen + 1]) == '.' || c == '~' || c == '8')) { + *usedlen += 2; + if (c == '8') { + continue; + } + pbuf = NULL; + // Need full path first (use expand_env() to remove a "~/") + if (!has_fullname && !has_homerelative) { + if (**fnamep == '~') { + p = pbuf = expand_env_save(*fnamep); + } else { + p = pbuf = FullName_save(*fnamep, false); + } + } else { + p = *fnamep; + } + + has_fullname = false; + + if (p != NULL) { + if (c == '.') { + os_dirname((char_u *)dirname, MAXPATHL); + if (has_homerelative) { + s = xstrdup(dirname); + home_replace(NULL, s, dirname, MAXPATHL, true); + xfree(s); + } + size_t namelen = STRLEN(dirname); + + // Do not call shorten_fname() here since it removes the prefix + // even though the path does not have a prefix. + if (FNAMENCMP(p, dirname, namelen) == 0) { + p += namelen; + if (vim_ispathsep(*p)) { + while (*p && vim_ispathsep(*p)) { + p++; + } + *fnamep = p; + if (pbuf != NULL) { + // free any allocated file name + xfree(*bufp); + *bufp = pbuf; + pbuf = NULL; + } + } + } + } else { + home_replace(NULL, p, dirname, MAXPATHL, true); + // Only replace it when it starts with '~' + if (*dirname == '~') { + s = xstrdup(dirname); + *fnamep = s; + xfree(*bufp); + *bufp = s; + has_homerelative = true; + } + } + xfree(pbuf); + } + } + + tail = path_tail(*fnamep); + *fnamelen = STRLEN(*fnamep); + + // ":h" - head, remove "/file_name", can be repeated + // Don't remove the first "/" or "c:\" + while (src[*usedlen] == ':' && src[*usedlen + 1] == 'h') { + valid |= VALID_HEAD; + *usedlen += 2; + s = (char *)get_past_head((char_u *)(*fnamep)); + while (tail > s && after_pathsep(s, tail)) { + MB_PTR_BACK(*fnamep, tail); + } + *fnamelen = (size_t)(tail - *fnamep); + if (*fnamelen == 0) { + // Result is empty. Turn it into "." to make ":cd %:h" work. + xfree(*bufp); + *bufp = *fnamep = tail = xstrdup("."); + *fnamelen = 1; + } else { + while (tail > s && !after_pathsep(s, tail)) { + MB_PTR_BACK(*fnamep, tail); + } + } + } + + // ":8" - shortname + if (src[*usedlen] == ':' && src[*usedlen + 1] == '8') { + *usedlen += 2; + } + + // ":t" - tail, just the basename + if (src[*usedlen] == ':' && src[*usedlen + 1] == 't') { + *usedlen += 2; + *fnamelen -= (size_t)(tail - *fnamep); + *fnamep = tail; + } + + // ":e" - extension, can be repeated + // ":r" - root, without extension, can be repeated + while (src[*usedlen] == ':' + && (src[*usedlen + 1] == 'e' || src[*usedlen + 1] == 'r')) { + /* find a '.' in the tail: + * - for second :e: before the current fname + * - otherwise: The last '.' + */ + const bool is_second_e = *fnamep > tail; + if (src[*usedlen + 1] == 'e' && is_second_e) { + s = (*fnamep) - 2; + } else { + s = (*fnamep) + *fnamelen - 1; + } + + for (; s > tail; s--) { + if (s[0] == '.') { + break; + } + } + if (src[*usedlen + 1] == 'e') { + if (s > tail || (0 && is_second_e && s == tail)) { + // we stopped at a '.' (so anchor to &'.' + 1) + char *newstart = s + 1; + size_t distance_stepped_back = (size_t)(*fnamep - newstart); + *fnamelen += distance_stepped_back; + *fnamep = newstart; + } else if (*fnamep <= tail) { + *fnamelen = 0; + } + } else { + // :r - Remove one extension + // + // Ensure that `s` doesn't go before `*fnamep`, + // since then we're taking too many roots: + // + // "path/to/this.file.ext" :e:e:r:r + // ^ ^-------- *fnamep + // +------------- tail + // + // Also ensure `s` doesn't go before `tail`, + // since then we're taking too many roots again: + // + // "path/to/this.file.ext" :r:r:r + // ^ ^------------- tail + // +--------------------- *fnamep + if (s > MAX(tail, (char *)(*fnamep))) { + *fnamelen = (size_t)(s - *fnamep); + } + } + *usedlen += 2; + } + + // ":s?pat?foo?" - substitute + // ":gs?pat?foo?" - global substitute + if (src[*usedlen] == ':' + && (src[*usedlen + 1] == 's' + || (src[*usedlen + 1] == 'g' && src[*usedlen + 2] == 's'))) { + int sep; + char *flags; + int didit = false; + + flags = ""; + s = src + *usedlen + 2; + if (src[*usedlen + 1] == 'g') { + flags = "g"; + s++; + } + + sep = (char_u)(*s++); + if (sep) { + // find end of pattern + p = vim_strchr(s, sep); + if (p != NULL) { + char *const pat = xstrnsave(s, (size_t)(p - s)); + s = p + 1; + // find end of substitution + p = vim_strchr(s, sep); + if (p != NULL) { + char *const sub = xstrnsave(s, (size_t)(p - s)); + char *const str = xstrnsave(*fnamep, *fnamelen); + *usedlen = (size_t)(p + 1 - src); + s = do_string_sub(str, pat, sub, NULL, flags); + *fnamep = s; + *fnamelen = STRLEN(s); + xfree(*bufp); + *bufp = s; + didit = TRUE; + xfree(sub); + xfree(str); + } + xfree(pat); + } + // after using ":s", repeat all the modifiers + if (didit) { + goto repeat; + } + } + } + + if (src[*usedlen] == ':' && src[*usedlen + 1] == 'S') { + // vim_strsave_shellescape() needs a NUL terminated string. + c = (char_u)(*fnamep)[*fnamelen]; + if (c != NUL) { + (*fnamep)[*fnamelen] = NUL; + } + p = (char *)vim_strsave_shellescape((char_u *)(*fnamep), false, false); + if (c != NUL) { + (*fnamep)[*fnamelen] = (char)c; + } + xfree(*bufp); + *bufp = *fnamep = p; + *fnamelen = STRLEN(p); + *usedlen += 2; + } + + return valid; +} + +/// Perform a substitution on "str" with pattern "pat" and substitute "sub". +/// When "sub" is NULL "expr" is used, must be a VAR_FUNC or VAR_PARTIAL. +/// "flags" can be "g" to do a global substitute. +/// +/// @return an allocated string, NULL for error. +char *do_string_sub(char *str, char *pat, char *sub, typval_T *expr, char *flags) +{ + int sublen; + regmatch_T regmatch; + int do_all; + char *tail; + char *end; + garray_T ga; + char *save_cpo; + char *zero_width = NULL; + + // Make 'cpoptions' empty, so that the 'l' flag doesn't work here + save_cpo = p_cpo; + p_cpo = (char *)empty_option; + + ga_init(&ga, 1, 200); + + do_all = (flags[0] == 'g'); + + regmatch.rm_ic = p_ic; + regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING); + if (regmatch.regprog != NULL) { + tail = str; + end = str + STRLEN(str); + while (vim_regexec_nl(®match, (char_u *)str, (colnr_T)(tail - str))) { + // Skip empty match except for first match. + if (regmatch.startp[0] == regmatch.endp[0]) { + if ((char_u *)zero_width == regmatch.startp[0]) { + // avoid getting stuck on a match with an empty string + int i = utfc_ptr2len(tail); + memmove((char_u *)ga.ga_data + ga.ga_len, tail, (size_t)i); + ga.ga_len += i; + tail += i; + continue; + } + zero_width = (char *)regmatch.startp[0]; + } + + // Get some space for a temporary buffer to do the substitution + // into. It will contain: + // - The text up to where the match is. + // - The substituted text. + // - The text after the match. + sublen = vim_regsub(®match, (char_u *)sub, expr, (char_u *)tail, 0, REGSUB_MAGIC); + ga_grow(&ga, (int)((end - tail) + sublen - + (regmatch.endp[0] - regmatch.startp[0]))); + + // copy the text up to where the match is + int i = (int)(regmatch.startp[0] - (char_u *)tail); + memmove((char_u *)ga.ga_data + ga.ga_len, tail, (size_t)i); + // add the substituted text + (void)vim_regsub(®match, (char_u *)sub, expr, + (char_u *)ga.ga_data + ga.ga_len + i, sublen, + REGSUB_COPY | REGSUB_MAGIC); + ga.ga_len += i + sublen - 1; + tail = (char *)regmatch.endp[0]; + if (*tail == NUL) { + break; + } + if (!do_all) { + break; + } + } + + if (ga.ga_data != NULL) { + STRCPY((char *)ga.ga_data + ga.ga_len, tail); + } + + vim_regfree(regmatch.regprog); + } + + char *ret = xstrdup(ga.ga_data == NULL ? str : ga.ga_data); + ga_clear(&ga); + if ((char_u *)p_cpo == empty_option) { + p_cpo = save_cpo; + } else { + // Darn, evaluating {sub} expression or {expr} changed the value. + free_string_option((char_u *)save_cpo); + } + + return ret; +} + +/// common code for getting job callbacks for jobstart, termopen and rpcstart +/// +/// @return true/false on success/failure. +bool common_job_callbacks(dict_T *vopts, CallbackReader *on_stdout, CallbackReader *on_stderr, + Callback *on_exit) +{ + if (tv_dict_get_callback(vopts, S_LEN("on_stdout"), &on_stdout->cb) + && tv_dict_get_callback(vopts, S_LEN("on_stderr"), &on_stderr->cb) + && tv_dict_get_callback(vopts, S_LEN("on_exit"), on_exit)) { + on_stdout->buffered = tv_dict_get_number(vopts, "stdout_buffered"); + on_stderr->buffered = tv_dict_get_number(vopts, "stderr_buffered"); + if (on_stdout->buffered && on_stdout->cb.type == kCallbackNone) { + on_stdout->self = vopts; + } + if (on_stderr->buffered && on_stderr->cb.type == kCallbackNone) { + on_stderr->self = vopts; + } + vopts->dv_refcount++; + return true; + } + + callback_reader_free(on_stdout); + callback_reader_free(on_stderr); + callback_free(on_exit); + return false; +} + +Channel *find_job(uint64_t id, bool show_error) +{ + Channel *data = find_channel(id); + if (!data || data->streamtype != kChannelStreamProc + || process_is_stopped(&data->stream.proc)) { + if (show_error) { + if (data && data->streamtype != kChannelStreamProc) { + emsg(_(e_invchanjob)); + } else { + emsg(_(e_invchan)); + } + } + return NULL; + } + return data; +} + +void script_host_eval(char *name, typval_T *argvars, typval_T *rettv) +{ + if (check_secure()) { + return; + } + + if (argvars[0].v_type != VAR_STRING) { + emsg(_(e_invarg)); + return; + } + + list_T *args = tv_list_alloc(1); + tv_list_append_string(args, (const char *)argvars[0].vval.v_string, -1); + *rettv = eval_call_provider(name, "eval", args, false); +} + +/// @param discard Clears the value returned by the provider and returns +/// an empty typval_T. +typval_T eval_call_provider(char *provider, char *method, list_T *arguments, bool discard) +{ + if (!eval_has_provider(provider)) { + semsg("E319: No \"%s\" provider found. Run \":checkhealth provider\"", + provider); + return (typval_T){ + .v_type = VAR_NUMBER, + .v_lock = VAR_UNLOCKED, + .vval.v_number = (varnumber_T)0 + }; + } + + char func[256]; + int name_len = snprintf(func, sizeof(func), "provider#%s#Call", provider); + + // Save caller scope information + struct caller_scope saved_provider_caller_scope = provider_caller_scope; + provider_caller_scope = (struct caller_scope) { + .script_ctx = current_sctx, + .sourcing_name = sourcing_name, + .sourcing_lnum = sourcing_lnum, + .autocmd_fname = autocmd_fname, + .autocmd_match = autocmd_match, + .autocmd_bufnr = autocmd_bufnr, + .funccalp = (void *)get_current_funccal() + }; + funccal_entry_T funccal_entry; + save_funccal(&funccal_entry); + provider_call_nesting++; + + typval_T argvars[3] = { + { .v_type = VAR_STRING, .vval.v_string = method, + .v_lock = VAR_UNLOCKED }, + { .v_type = VAR_LIST, .vval.v_list = arguments, .v_lock = VAR_UNLOCKED }, + { .v_type = VAR_UNKNOWN } + }; + typval_T rettv = { .v_type = VAR_UNKNOWN, .v_lock = VAR_UNLOCKED }; + tv_list_ref(arguments); + + funcexe_T funcexe = FUNCEXE_INIT; + funcexe.firstline = curwin->w_cursor.lnum; + funcexe.lastline = curwin->w_cursor.lnum; + funcexe.evaluate = true; + (void)call_func(func, name_len, &rettv, 2, argvars, &funcexe); + + tv_list_unref(arguments); + // Restore caller scope information + restore_funccal(); + provider_caller_scope = saved_provider_caller_scope; + provider_call_nesting--; + assert(provider_call_nesting >= 0); + + if (discard) { + tv_clear(&rettv); + } + + return rettv; +} + +/// Checks if provider for feature `feat` is enabled. +bool eval_has_provider(const char *feat) +{ + if (!strequal(feat, "clipboard") + && !strequal(feat, "python3") + && !strequal(feat, "python3_compiled") + && !strequal(feat, "python3_dynamic") + && !strequal(feat, "perl") + && !strequal(feat, "ruby") + && !strequal(feat, "node")) { + // Avoid autoload for non-provider has() features. + return false; + } + + char name[32]; // Normalized: "python_compiled" => "python". + snprintf(name, sizeof(name), "%s", feat); + strchrsub(name, '_', '\0'); // Chop any "_xx" suffix. + + char buf[256]; + typval_T tv; + // Get the g:loaded_xx_provider variable. + int len = snprintf(buf, sizeof(buf), "g:loaded_%s_provider", name); + if (get_var_tv(buf, len, &tv, NULL, false, true) == FAIL) { + // Trigger autoload once. + len = snprintf(buf, sizeof(buf), "provider#%s#bogus", name); + script_autoload(buf, (size_t)len, false); + + // Retry the (non-autoload-style) variable. + len = snprintf(buf, sizeof(buf), "g:loaded_%s_provider", name); + if (get_var_tv(buf, len, &tv, NULL, false, true) == FAIL) { + // Show a hint if Call() is defined but g:loaded_xx_provider is missing. + snprintf(buf, sizeof(buf), "provider#%s#Call", name); + if (!!find_func((char_u *)buf) && p_lpl) { + semsg("provider: %s: missing required variable g:loaded_%s_provider", + name, name); + } + return false; + } + } + + bool ok = (tv.v_type == VAR_NUMBER) + ? 2 == tv.vval.v_number // Value of 2 means "loaded and working". + : false; + + if (ok) { + // Call() must be defined if provider claims to be working. + snprintf(buf, sizeof(buf), "provider#%s#Call", name); + if (!find_func((char_u *)buf)) { + semsg("provider: %s: g:loaded_%s_provider=2 but %s is not defined", + name, name, buf); + ok = false; + } + } + + return ok; +} + +/// Writes "<sourcing_name>:<sourcing_lnum>" to `buf[bufsize]`. +void eval_fmt_source_name_line(char *buf, size_t bufsize) +{ + if (sourcing_name) { + snprintf(buf, bufsize, "%s:%" PRIdLINENR, sourcing_name, sourcing_lnum); + } else { + snprintf(buf, bufsize, "?"); + } +} + +/// ":checkhealth [plugins]" +void ex_checkhealth(exarg_T *eap) +{ + bool found = !!find_func((char_u *)"health#check"); + if (!found + && script_autoload("health#check", sizeof("health#check") - 1, false)) { + found = !!find_func((char_u *)"health#check"); + } + if (!found) { + const char *vimruntime_env = os_getenv("VIMRUNTIME"); + if (vimruntime_env == NULL) { + emsg(_("E5009: $VIMRUNTIME is empty or unset")); + } else { + bool rtp_ok = NULL != strstr((char *)p_rtp, vimruntime_env); + if (rtp_ok) { + semsg(_("E5009: Invalid $VIMRUNTIME: %s"), vimruntime_env); + } else { + emsg(_("E5009: Invalid 'runtimepath'")); + } + } + return; + } + + size_t bufsize = STRLEN(eap->arg) + sizeof("call health#check('')"); + char *buf = xmalloc(bufsize); + snprintf(buf, bufsize, "call health#check('%s')", eap->arg); + + do_cmdline_cmd(buf); + + xfree(buf); +} + +void invoke_prompt_callback(void) +{ + typval_T rettv; + typval_T argv[2]; + char *text; + char *prompt; + linenr_T lnum = curbuf->b_ml.ml_line_count; + + // Add a new line for the prompt before invoking the callback, so that + // text can always be inserted above the last line. + ml_append(lnum, "", 0, false); + curwin->w_cursor.lnum = lnum + 1; + curwin->w_cursor.col = 0; + + if (curbuf->b_prompt_callback.type == kCallbackNone) { + return; + } + text = (char *)ml_get(lnum); + prompt = (char *)prompt_text(); + if (STRLEN(text) >= STRLEN(prompt)) { + text += STRLEN(prompt); + } + argv[0].v_type = VAR_STRING; + argv[0].vval.v_string = xstrdup(text); + argv[1].v_type = VAR_UNKNOWN; + + callback_call(&curbuf->b_prompt_callback, 1, argv, &rettv); + tv_clear(&argv[0]); + tv_clear(&rettv); +} + +/// @return true when the interrupt callback was invoked. +bool invoke_prompt_interrupt(void) +{ + typval_T rettv; + typval_T argv[1]; + + if (curbuf->b_prompt_interrupt.type == kCallbackNone) { + return false; + } + argv[0].v_type = VAR_UNKNOWN; + + got_int = false; // don't skip executing commands + callback_call(&curbuf->b_prompt_interrupt, 0, argv, &rettv); + tv_clear(&rettv); + return true; +} + +/// Compare "typ1" and "typ2". Put the result in "typ1". +/// +/// @param typ1 first operand +/// @param typ2 second operand +/// @param type operator +/// @param ic ignore case +int typval_compare(typval_T *typ1, typval_T *typ2, exprtype_T type, bool ic) + FUNC_ATTR_NONNULL_ALL +{ + varnumber_T n1, n2; + const bool type_is = type == EXPR_IS || type == EXPR_ISNOT; + + if (type_is && typ1->v_type != typ2->v_type) { + // For "is" a different type always means false, for "notis" + // it means true. + n1 = type == EXPR_ISNOT; + } else if (typ1->v_type == VAR_BLOB || typ2->v_type == VAR_BLOB) { + if (type_is) { + n1 = typ1->v_type == typ2->v_type + && typ1->vval.v_blob == typ2->vval.v_blob; + if (type == EXPR_ISNOT) { + n1 = !n1; + } + } else if (typ1->v_type != typ2->v_type + || (type != EXPR_EQUAL && type != EXPR_NEQUAL)) { + if (typ1->v_type != typ2->v_type) { + emsg(_("E977: Can only compare Blob with Blob")); + } else { + emsg(_(e_invalblob)); + } + tv_clear(typ1); + return FAIL; + } else { + // Compare two Blobs for being equal or unequal. + n1 = tv_blob_equal(typ1->vval.v_blob, typ2->vval.v_blob); + if (type == EXPR_NEQUAL) { + n1 = !n1; + } + } + } else if (typ1->v_type == VAR_LIST || typ2->v_type == VAR_LIST) { + if (type_is) { + n1 = typ1->v_type == typ2->v_type + && typ1->vval.v_list == typ2->vval.v_list; + if (type == EXPR_ISNOT) { + n1 = !n1; + } + } else if (typ1->v_type != typ2->v_type + || (type != EXPR_EQUAL && type != EXPR_NEQUAL)) { + if (typ1->v_type != typ2->v_type) { + emsg(_("E691: Can only compare List with List")); + } else { + emsg(_("E692: Invalid operation for List")); + } + tv_clear(typ1); + return FAIL; + } else { + // Compare two Lists for being equal or unequal. + n1 = tv_list_equal(typ1->vval.v_list, typ2->vval.v_list, ic, false); + if (type == EXPR_NEQUAL) { + n1 = !n1; + } + } + } else if (typ1->v_type == VAR_DICT || typ2->v_type == VAR_DICT) { + if (type_is) { + n1 = typ1->v_type == typ2->v_type + && typ1->vval.v_dict == typ2->vval.v_dict; + if (type == EXPR_ISNOT) { + n1 = !n1; + } + } else if (typ1->v_type != typ2->v_type + || (type != EXPR_EQUAL && type != EXPR_NEQUAL)) { + if (typ1->v_type != typ2->v_type) { + emsg(_("E735: Can only compare Dictionary with Dictionary")); + } else { + emsg(_("E736: Invalid operation for Dictionary")); + } + tv_clear(typ1); + return FAIL; + } else { + // Compare two Dictionaries for being equal or unequal. + n1 = tv_dict_equal(typ1->vval.v_dict, typ2->vval.v_dict, ic, false); + if (type == EXPR_NEQUAL) { + n1 = !n1; + } + } + } else if (tv_is_func(*typ1) || tv_is_func(*typ2)) { + if (type != EXPR_EQUAL && type != EXPR_NEQUAL + && type != EXPR_IS && type != EXPR_ISNOT) { + emsg(_("E694: Invalid operation for Funcrefs")); + tv_clear(typ1); + return FAIL; + } + if ((typ1->v_type == VAR_PARTIAL && typ1->vval.v_partial == NULL) + || (typ2->v_type == VAR_PARTIAL && typ2->vval.v_partial == NULL)) { + // when a partial is NULL assume not equal + n1 = false; + } else if (type_is) { + if (typ1->v_type == VAR_FUNC && typ2->v_type == VAR_FUNC) { + // strings are considered the same if their value is + // the same + n1 = tv_equal(typ1, typ2, ic, false); + } else if (typ1->v_type == VAR_PARTIAL && typ2->v_type == VAR_PARTIAL) { + n1 = typ1->vval.v_partial == typ2->vval.v_partial; + } else { + n1 = false; + } + } else { + n1 = tv_equal(typ1, typ2, ic, false); + } + if (type == EXPR_NEQUAL || type == EXPR_ISNOT) { + n1 = !n1; + } + } else if ((typ1->v_type == VAR_FLOAT || typ2->v_type == VAR_FLOAT) + && type != EXPR_MATCH && type != EXPR_NOMATCH) { + // If one of the two variables is a float, compare as a float. + // When using "=~" or "!~", always compare as string. + const float_T f1 = tv_get_float(typ1); + const float_T f2 = tv_get_float(typ2); + n1 = false; + switch (type) { + case EXPR_IS: + case EXPR_EQUAL: + n1 = f1 == f2; break; + case EXPR_ISNOT: + case EXPR_NEQUAL: + n1 = f1 != f2; break; + case EXPR_GREATER: + n1 = f1 > f2; break; + case EXPR_GEQUAL: + n1 = f1 >= f2; break; + case EXPR_SMALLER: + n1 = f1 < f2; break; + case EXPR_SEQUAL: + n1 = f1 <= f2; break; + case EXPR_UNKNOWN: + case EXPR_MATCH: + case EXPR_NOMATCH: + break; // avoid gcc warning + } + } else if ((typ1->v_type == VAR_NUMBER || typ2->v_type == VAR_NUMBER) + && type != EXPR_MATCH && type != EXPR_NOMATCH) { + // If one of the two variables is a number, compare as a number. + // When using "=~" or "!~", always compare as string. + n1 = tv_get_number(typ1); + n2 = tv_get_number(typ2); + switch (type) { + case EXPR_IS: + case EXPR_EQUAL: + n1 = n1 == n2; break; + case EXPR_ISNOT: + case EXPR_NEQUAL: + n1 = n1 != n2; break; + case EXPR_GREATER: + n1 = n1 > n2; break; + case EXPR_GEQUAL: + n1 = n1 >= n2; break; + case EXPR_SMALLER: + n1 = n1 < n2; break; + case EXPR_SEQUAL: + n1 = n1 <= n2; break; + case EXPR_UNKNOWN: + case EXPR_MATCH: + case EXPR_NOMATCH: + break; // avoid gcc warning + } + } else { + char buf1[NUMBUFLEN]; + char buf2[NUMBUFLEN]; + const char *const s1 = tv_get_string_buf(typ1, buf1); + const char *const s2 = tv_get_string_buf(typ2, buf2); + int i; + if (type != EXPR_MATCH && type != EXPR_NOMATCH) { + i = mb_strcmp_ic(ic, s1, s2); + } else { + i = 0; + } + n1 = false; + switch (type) { + case EXPR_IS: + case EXPR_EQUAL: + n1 = i == 0; break; + case EXPR_ISNOT: + case EXPR_NEQUAL: + n1 = i != 0; break; + case EXPR_GREATER: + n1 = i > 0; break; + case EXPR_GEQUAL: + n1 = i >= 0; break; + case EXPR_SMALLER: + n1 = i < 0; break; + case EXPR_SEQUAL: + n1 = i <= 0; break; + + case EXPR_MATCH: + case EXPR_NOMATCH: + n1 = pattern_match((char *)s2, (char *)s1, ic); + if (type == EXPR_NOMATCH) { + n1 = !n1; + } + break; + case EXPR_UNKNOWN: + break; // avoid gcc warning + } + } + tv_clear(typ1); + typ1->v_type = VAR_NUMBER; + typ1->vval.v_number = n1; + return OK; +} + +char *typval_tostring(typval_T *arg) +{ + if (arg == NULL) { + return xstrdup("(does not exist)"); + } + return encode_tv2string(arg, NULL); +} diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index b3cfec8709..691ccfe535 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -4123,6 +4123,9 @@ static void f_has(typval_T *argvars, typval_T *rettv, FunPtr fptr) "winaltkeys", "writebackup", "nvim", + "colorcolchar", + "omnihighlight", + "userreg", }; // XXX: eval_has_provider() may shell out :( diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index dec3ef68aa..d01fff6b94 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -385,7 +385,7 @@ const char *skip_var_list(const char *arg, int *var_count, int *semicolon) static const char *skip_var_one(const char *arg) { if (*arg == '@' && arg[1] != NUL) { - return arg + 2; + return arg + 1 + utfc_ptr2len(arg + 1); } return (char *)find_name_end(*arg == '$' || *arg == '&' ? arg + 1 : arg, NULL, NULL, FNE_INCL_BR | FNE_CHECK_START); @@ -694,10 +694,14 @@ static char *ex_let_one(char *arg, typval_T *const tv, const bool copy, const bo return NULL; } arg++; + + int regname = utf_ptr2char(arg); + int mblen = utf_ptr2len(arg); + if (op != NULL && vim_strchr("+-*/%", *op) != NULL) { semsg(_(e_letwrong), op); } else if (endchars != NULL - && vim_strchr(endchars, *skipwhite(arg + 1)) == NULL) { + && vim_strchr(endchars, *skipwhite(arg + mblen)) == NULL) { emsg(_(e_letunexp)); } else { char *s; @@ -705,7 +709,7 @@ static char *ex_let_one(char *arg, typval_T *const tv, const bool copy, const bo char *ptofree = NULL; const char *p = tv_get_string_chk(tv); if (p != NULL && op != NULL && *op == '.') { - s = get_reg_contents(*arg == '@' ? '"' : *arg, kGRegExprSrc); + s = get_reg_contents(*arg == '@' ? '"' : regname, kGRegExprSrc); if (s != NULL) { ptofree = (char *)concat_str((char_u *)s, (const char_u *)p); p = (const char *)ptofree; @@ -713,9 +717,9 @@ static char *ex_let_one(char *arg, typval_T *const tv, const bool copy, const bo } } if (p != NULL) { - write_reg_contents(*arg == '@' ? '"' : *arg, + write_reg_contents(*arg == '@' ? '"' : regname, (const char_u *)p, (ssize_t)STRLEN(p), false); - arg_end = arg + 1; + arg_end = arg + mblen; } xfree(ptofree); } diff --git a/src/nvim/generators/gen_options.lua b/src/nvim/generators/gen_options.lua index 0454c54faf..51ffdae3b0 100644 --- a/src/nvim/generators/gen_options.lua +++ b/src/nvim/generators/gen_options.lua @@ -183,6 +183,26 @@ local dump_option = function(i, o) w(' },') end +-- Verify options are correctly ordered. The options with the same first letter +-- must be together or else some of the options may seem to disappear from +-- Neovim. +done_chrs = {} +last_opt = nil +for _, o in ipairs(options.options) do + if (last_opt == nil) then + last_opt = o + elseif last_opt.full_name:sub(1, 1) ~= o.full_name:sub(1, 1) then + done_chrs[last_opt.full_name:sub(1, 1)] = true + last_opt = o + + if done_chrs[o.full_name:sub(1, 1)] then + print("Option '" .. o.full_name .. "' must be next to options with the " + .. "same starting character.") + os.exit(1) + end + end +end + w('static vimoption_T options[] = {') for i, o in ipairs(options.options) do dump_option(i, o) diff --git a/src/nvim/keycodes.c b/src/nvim/keycodes.c index 5a5257efb2..9899f10788 100644 --- a/src/nvim/keycodes.c +++ b/src/nvim/keycodes.c @@ -248,6 +248,33 @@ static const struct key_name_entry { { K_F62, "F62" }, { K_F63, "F63" }, + { K_F38, "F38" }, + { K_F39, "F39" }, + { K_F40, "F40" }, + { K_F41, "F41" }, + { K_F42, "F42" }, + { K_F43, "F43" }, + { K_F44, "F44" }, + { K_F45, "F45" }, + { K_F46, "F46" }, + { K_F47, "F47" }, + { K_F48, "F48" }, + { K_F49, "F49" }, + { K_F50, "F50" }, + { K_F51, "F51" }, + { K_F52, "F52" }, + { K_F53, "F53" }, + { K_F54, "F54" }, + { K_F55, "F55" }, + { K_F56, "F56" }, + { K_F57, "F57" }, + { K_F58, "F58" }, + { K_F59, "F59" }, + { K_F60, "F60" }, + { K_F61, "F61" }, + { K_F62, "F62" }, + { K_F63, "F63" }, + { K_XF1, "xF1" }, { K_XF2, "xF2" }, { K_XF3, "xF3" }, diff --git a/src/nvim/keycodes.h b/src/nvim/keycodes.h index c4d984ee17..943c127325 100644 --- a/src/nvim/keycodes.h +++ b/src/nvim/keycodes.h @@ -329,6 +329,33 @@ enum key_extra { #define K_F62 TERMCAP2KEY('F', 'q') #define K_F63 TERMCAP2KEY('F', 'r') +#define K_F38 TERMCAP2KEY('F', 'S') +#define K_F39 TERMCAP2KEY('F', 'T') +#define K_F40 TERMCAP2KEY('F', 'U') +#define K_F41 TERMCAP2KEY('F', 'V') +#define K_F42 TERMCAP2KEY('F', 'W') +#define K_F43 TERMCAP2KEY('F', 'X') +#define K_F44 TERMCAP2KEY('F', 'Y') +#define K_F45 TERMCAP2KEY('F', 'Z') +#define K_F46 TERMCAP2KEY('F', 'a') +#define K_F47 TERMCAP2KEY('F', 'b') +#define K_F48 TERMCAP2KEY('F', 'c') +#define K_F49 TERMCAP2KEY('F', 'd') +#define K_F50 TERMCAP2KEY('F', 'e') +#define K_F51 TERMCAP2KEY('F', 'f') +#define K_F52 TERMCAP2KEY('F', 'g') +#define K_F53 TERMCAP2KEY('F', 'h') +#define K_F54 TERMCAP2KEY('F', 'i') +#define K_F55 TERMCAP2KEY('F', 'j') +#define K_F56 TERMCAP2KEY('F', 'k') +#define K_F57 TERMCAP2KEY('F', 'l') +#define K_F58 TERMCAP2KEY('F', 'm') +#define K_F59 TERMCAP2KEY('F', 'n') +#define K_F60 TERMCAP2KEY('F', 'o') +#define K_F61 TERMCAP2KEY('F', 'p') +#define K_F62 TERMCAP2KEY('F', 'q') +#define K_F63 TERMCAP2KEY('F', 'r') + // extra set of shifted function keys F1-F4, for vt100 compatible xterm #define K_S_XF1 TERMCAP2KEY(KS_EXTRA, KE_S_XF1) #define K_S_XF2 TERMCAP2KEY(KS_EXTRA, KE_S_XF2) diff --git a/src/nvim/map.c b/src/nvim/map.c index d3058a5d52..86e7317b56 100644 --- a/src/nvim/map.c +++ b/src/nvim/map.c @@ -163,6 +163,8 @@ static inline bool ColorKey_eq(ColorKey ae1, ColorKey ae2) return memcmp(&ae1, &ae2, sizeof(ae1)) == 0; } +MAP_IMPL(ptr_t, int, DEFAULT_INITIALIZER) +MAP_IMPL(int, ptr_t, DEFAULT_INITIALIZER) MAP_IMPL(int, int, DEFAULT_INITIALIZER) MAP_IMPL(int, cstr_t, DEFAULT_INITIALIZER) MAP_IMPL(cstr_t, ptr_t, DEFAULT_INITIALIZER) diff --git a/src/nvim/map.h b/src/nvim/map.h index 845daac3f7..acc796bd98 100644 --- a/src/nvim/map.h +++ b/src/nvim/map.h @@ -35,6 +35,8 @@ // NOTE: Keys AND values must be allocated! khash.h does not make a copy. // MAP_DECLS(int, int) +MAP_DECLS(int, ptr_t) +MAP_DECLS(ptr_t, int) MAP_DECLS(int, cstr_t) MAP_DECLS(cstr_t, ptr_t) MAP_DECLS(cstr_t, int) diff --git a/src/nvim/ops.c b/src/nvim/ops.c index 53612be697..9a44c36d44 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -56,8 +56,24 @@ #include "nvim/undo.h" #include "nvim/vim.h" #include "nvim/window.h" +#include "nvim/yankmap.h" -static yankreg_T y_regs[NUM_REGISTERS] = { 0 }; +struct yank_registers { + yankmap_T inner; +}; + +yank_registers_T y_regs; + +static yankreg_T *get_reg(yank_registers_T *regs, int idx) +{ + return yankmap_get(®s->inner, idx); + +} + +static yankreg_T *get_global_reg(int idx) +{ + return get_reg(&y_regs, idx); +} static yankreg_T *y_previous = NULL; // ptr to last written yankreg @@ -771,6 +787,24 @@ char_u *get_expr_line_src(void) return vim_strsave(expr_line); } + +int get_userreg(int regname) +{ + if ((regname >= 'a' && regname <= 'z') + || (regname >= 'A' && regname <= 'Z') + || (regname >= '0' && regname <= '9') + || (regname <= 127 && strchr("\"-:.%#=*+_/", regname)) + || regname == Ctrl_F + || regname == Ctrl_P + || regname == Ctrl_W + || regname == Ctrl_A + || (regname + USER_REGISTERS_START) < regname) { + return -1; + } + + return regname + USER_REGISTERS_START; +} + /// @return whether `regname` is a valid name of a yank register. /// /// @note: There is no check for 0 (default register), caller should do this. @@ -787,7 +821,8 @@ bool valid_yank_reg(int regname, bool writing) || regname == '-' || regname == '_' || regname == '*' - || regname == '+') { + || regname == '+' + || get_userreg(regname) != -1) { return true; } return false; @@ -830,7 +865,7 @@ yankreg_T *get_yank_register(int regname, int mode) if (i == -1) { i = 0; } - reg = &y_regs[i]; + reg = get_global_reg(i); if (mode == YREG_YANK) { // remember the written register for unnamed paste @@ -1348,7 +1383,96 @@ static void stuffescaped(const char *arg, bool literally) /// @param errmsg give error message when failing /// /// @return true if "regname" is a special register, -bool get_spec_reg(int regname, char_u **argp, bool *allocated, bool errmsg) +bool get_spec_reg(int regname, char_u **argp, bool *allocated, bool errmsg); + +/* + * Executes a call to the put() function on a user-defined register to get the + * contents of a user defined register. + */ +static int eval_urf_put(char_u *ufn, int regname, char_u **argp) +{ + char regname_str[5]; + int len; + + len = (*utf_char2len)(regname); + regname_str[len] = 0; + utf_char2bytes(regname, (char*) regname_str); + + typval_T args[3]; + args[0].v_type = VAR_STRING; + args[1].v_type = VAR_STRING; + args[2].v_type = VAR_UNKNOWN; + + args[0].vval.v_string = "put"; + args[1].vval.v_string = regname_str; + + *argp = (char_u *)call_func_retstr((char *)ufn, 3, args); + return *argp == NULL; +} + +/* + * Executes the yank() function on a user-defined register to set the contents + * of that register. + */ +static int eval_yank_userreg(const char_u *ufn, int regname, yankreg_T *reg) +{ + if (!reg) + return -1; + + char *totalbuf; + size_t totallen = 0; + size_t i, j, k; + int ret, len; + char regname_str[5]; + + { + // Concat the contents of the register to pass into the yank() + // user-defined function. + for (i = 0; i < reg->y_size; ++i) { + totallen += strlen((char *)reg->y_array[i]) + 1; + } + totalbuf = xmalloc(sizeof(char_u) * totallen); + j = 0; + for (i = 0; i < reg->y_size; ++i) { + for (k = 0; reg->y_array[i][k] != 0; ++k, ++j) { + totalbuf[j] = reg->y_array[i][k]; + } + if (i < reg->y_size - 1) { + totalbuf[j++] = '\n'; + } + } + totalbuf[j++] = 0; + } + + len = (*utf_char2len)(regname); + regname_str[len] = 0; + utf_char2bytes(regname, regname_str); + + typval_T args[4]; + args[0].v_type = VAR_STRING; + args[1].v_type = VAR_STRING; + args[2].v_type = VAR_STRING; + args[3].v_type = VAR_UNKNOWN; + + args[0].vval.v_string = "yank"; + args[1].vval.v_string = regname_str; + args[2].vval.v_string = totalbuf; + + char *dup_ufn = strdup((char *)ufn); + ret = (int)call_func_retnr(dup_ufn, 3, args); + xfree(dup_ufn); + xfree(totalbuf); + return ret; +} + +// If "regname" is a special register, return true and store a pointer to its +// value in "argp". +bool get_spec_reg( + int regname, + char_u **argp, + bool *allocated, // return: true when value was allocated + bool errmsg // give error message when failing +) { size_t cnt; @@ -1426,6 +1550,15 @@ bool get_spec_reg(int regname, char_u **argp, bool *allocated, bool errmsg) case '_': // black hole: always empty *argp = (char_u *)""; return true; + + default: /* User-defined registers. */ + if (get_userreg(regname) != -1) { + if (!curbuf->b_p_urf || strlen((char *) curbuf->b_p_urf) == 0) + return false; + eval_urf_put(curbuf->b_p_urf, regname, argp); + *allocated = true; + return true; + } } return false; @@ -1472,14 +1605,14 @@ bool cmdline_paste_reg(int regname, bool literally_arg, bool remcr) /// Shift the delete registers: "9 is cleared, "8 becomes "9, etc. static void shift_delete_registers(bool y_append) { - free_register(&y_regs[9]); // free register "9 + free_register(get_global_reg(9)); // free register "9 for (int n = 9; n > 1; n--) { - y_regs[n] = y_regs[n - 1]; + *get_global_reg(n) = *get_global_reg(n - 1); } if (!y_append) { - y_previous = &y_regs[1]; + y_previous = get_global_reg(1); } - y_regs[1].y_array = NULL; // set register "1 to empty + get_global_reg(1)->y_array = NULL; // set register "1 to empty } /// Handle a delete operation. @@ -1580,7 +1713,7 @@ int op_delete(oparg_T *oap) // compatible) if (oap->motion_type == kMTLineWise || oap->line_count > 1 || oap->use_reg_one) { shift_delete_registers(is_append_register(oap->regname)); - reg = &y_regs[1]; + reg = get_global_reg(1); op_yank_reg(oap, false, reg, false); did_yank = true; } @@ -2585,13 +2718,22 @@ int op_change(oparg_T *oap) return retval; } + +/* + * set all the yank registers to empty (called from main()) + */ +void init_yank(void) +{ + init_yankmap(&y_regs.inner); +} + #if defined(EXITFREE) void clear_registers(void) { int i; for (i = 0; i < NUM_REGISTERS; i++) { - free_register(&y_regs[i]); + free_register(get_global_reg(i)); } } @@ -2635,6 +2777,11 @@ bool op_yank(oparg_T *oap, bool message) yankreg_T *reg = get_yank_register(oap->regname, YREG_YANK); op_yank_reg(oap, message, reg, is_append_register(oap->regname)); + + if (get_userreg(oap->regname) != -1) { + return eval_yank_userreg(curbuf->b_p_urf, oap->regname, reg) != -1; + } + set_clipboard(oap->regname, reg); do_autocmd_textyankpost(oap, reg); @@ -3087,7 +3234,7 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) if (insert_string != NULL) { y_type = kMTCharWise; - if (regname == '=') { + if (regname == '=' || get_userreg(regname) != -1) { /* For the = register we need to split the string at NL * characters. * Loop twice: count the number of lines and save them. */ @@ -3784,7 +3931,7 @@ int get_register_name(int num) /// @return the index of the register "" points to. int get_unname_register(void) { - return y_previous == NULL ? -1 : (int)(y_previous - &y_regs[0]); + return yankmap_find(&y_regs.inner, y_previous); } /// ":dis" and ":registers": Display the contents of the yank registers. @@ -3823,10 +3970,10 @@ void ex_display(exarg_T *eap) if (y_previous != NULL) { yb = y_previous; } else { - yb = &(y_regs[0]); + yb = get_global_reg(0); } } else { - yb = &(y_regs[i]); + yb = get_global_reg(i); } get_clipboard(name, &yb, true); @@ -5543,6 +5690,10 @@ static void finish_write_reg(int name, yankreg_T *reg, yankreg_T *old_y_previous // Send text of clipboard register to the clipboard. set_clipboard(name, reg); + if (get_userreg(name) != -1) { + eval_yank_userreg(curbuf->b_p_urf, name, reg); + } + // ':let @" = "val"' should change the meaning of the "" register if (name != '"') { y_previous = old_y_previous; @@ -6947,7 +7098,7 @@ static yankreg_T *adjust_clipboard_name(int *name, bool quiet, bool writing) } if (explicit_cb_reg) { - target = &y_regs[*name == '*' ? STAR_REGISTER : PLUS_REGISTER]; + target = get_global_reg(*name == '*' ? STAR_REGISTER : PLUS_REGISTER); if (writing && (cb_flags & (*name == '*' ? CB_UNNAMED : CB_UNNAMEDPLUS))) { clipboard_needs_update = false; } @@ -6964,10 +7115,10 @@ static yankreg_T *adjust_clipboard_name(int *name, bool quiet, bool writing) if (cb_flags & CB_UNNAMEDPLUS) { *name = (cb_flags & CB_UNNAMED && writing) ? '"': '+'; - target = &y_regs[PLUS_REGISTER]; + target = get_global_reg(PLUS_REGISTER); } else { *name = '*'; - target = &y_regs[STAR_REGISTER]; + target = get_global_reg(STAR_REGISTER); } goto end; } @@ -7283,11 +7434,11 @@ static inline bool reg_empty(const yankreg_T *const reg) /// Iterate over global registers. /// /// @see op_register_iter -const void *op_global_reg_iter(const void *const iter, char *const name, yankreg_T *const reg, - bool *is_unnamed) +iter_register_T op_global_reg_iter(iter_register_T iter, char *const name, + yankreg_T *const reg, bool *is_unnamed) FUNC_ATTR_NONNULL_ARG(2, 3, 4) FUNC_ATTR_WARN_UNUSED_RESULT { - return op_reg_iter(iter, y_regs, name, reg, is_unnamed); + return op_reg_iter(iter, &y_regs, name, reg, is_unnamed); } /// Iterate over registers `regs`. @@ -7299,30 +7450,31 @@ const void *op_global_reg_iter(const void *const iter, char *const name, yankreg /// /// @return Pointer that must be passed to next `op_register_iter` call or /// NULL if iteration is over. -const void *op_reg_iter(const void *const iter, const yankreg_T *const regs, char *const name, - yankreg_T *const reg, bool *is_unnamed) +iter_register_T op_reg_iter(iter_register_T iter, yank_registers_T *regs, + char *const name, yankreg_T *const reg, + bool *is_unnamed) FUNC_ATTR_NONNULL_ARG(3, 4, 5) FUNC_ATTR_WARN_UNUSED_RESULT { *name = NUL; - const yankreg_T *iter_reg = (iter == NULL - ? &(regs[0]) - : (const yankreg_T *const)iter); - while (iter_reg - &(regs[0]) < NUM_SAVED_REGISTERS && reg_empty(iter_reg)) { - iter_reg++; - } - if (iter_reg - &(regs[0]) == NUM_SAVED_REGISTERS || reg_empty(iter_reg)) { - return NULL; - } - int iter_off = (int)(iter_reg - &(regs[0])); - *name = (char)get_register_name(iter_off); - *reg = *iter_reg; - *is_unnamed = (iter_reg == y_previous); - while (++iter_reg - &(regs[0]) < NUM_SAVED_REGISTERS) { - if (!reg_empty(iter_reg)) { - return (void *)iter_reg; + int iter_idx = (int)(iter == ITER_REGISTER_NULL ? 0 : iter - 1); + + while (iter_idx < NUM_SAVED_REGISTERS && reg_empty(get_reg(regs, iter_idx))) + ++iter_idx; + + if (iter_idx >= NUM_SAVED_REGISTERS || reg_empty(get_reg(regs, iter_idx))) + return ITER_REGISTER_NULL; + + *reg = *get_reg(regs, iter_idx); + *name = (char)get_register_name((int)iter_idx); + *is_unnamed = (get_reg(regs, iter_idx) == y_previous); + + while (++iter_idx < NUM_SAVED_REGISTERS) { + if (!reg_empty(get_reg(regs, iter_idx))) { + return (iter_register_T)(iter_idx + 1); } } - return NULL; + + return ITER_REGISTER_NULL; } /// Get a number of non-empty registers @@ -7330,8 +7482,8 @@ size_t op_reg_amount(void) FUNC_ATTR_WARN_UNUSED_RESULT { size_t ret = 0; - for (size_t i = 0; i < NUM_SAVED_REGISTERS; i++) { - if (!reg_empty(y_regs + i)) { + for (int i = 0; i < NUM_SAVED_REGISTERS; i++) { + if (!reg_empty(get_global_reg(i))) { ret++; } } @@ -7351,11 +7503,11 @@ bool op_reg_set(const char name, const yankreg_T reg, bool is_unnamed) if (i == -1) { return false; } - free_register(&y_regs[i]); - y_regs[i] = reg; + free_register(get_global_reg(i)); + *get_global_reg(i) = reg; if (is_unnamed) { - y_previous = &y_regs[i]; + y_previous = get_global_reg(i); } return true; } @@ -7371,7 +7523,7 @@ const yankreg_T *op_reg_get(const char name) if (i == -1) { return NULL; } - return &y_regs[i]; + return get_global_reg(i); } /// Set the previous yank register @@ -7386,7 +7538,7 @@ bool op_reg_set_previous(const char name) return false; } - y_previous = &y_regs[i]; + y_previous = get_global_reg(i); return true; } diff --git a/src/nvim/ops.h b/src/nvim/ops.h index 05893c9940..a456d68003 100644 --- a/src/nvim/ops.h +++ b/src/nvim/ops.h @@ -22,6 +22,7 @@ typedef int (*Indenter)(void); #define PUT_LINE_SPLIT 16 // split line for linewise register #define PUT_LINE_FORWARD 32 // put linewise register below Visual sel. #define PUT_BLOCK_INNER 64 // in block mode, do not add trailing spaces +#define ITER_REGISTER_NULL 0 /* * Registers: @@ -37,7 +38,8 @@ typedef int (*Indenter)(void); // The following registers should not be saved in ShaDa file: #define STAR_REGISTER 37 #define PLUS_REGISTER 38 -#define NUM_REGISTERS 39 +#define USER_REGISTERS_START 39 +#define NUM_REGISTERS USER_REGISTERS_START // Operator IDs; The order must correspond to opchars[] in ops.c! #define OP_NOP 0 // no pending operation @@ -96,6 +98,8 @@ typedef enum { YREG_YANK, YREG_PUT, } yreg_mode_t; +/// Returns a reference to a user-defined register. +int get_userreg(const int regname); /// Convert register name into register index /// @@ -118,10 +122,15 @@ static inline int op_reg_index(const int regname) } else if (regname == '+') { return PLUS_REGISTER; } else { - return -1; + return get_userreg(regname); } } +struct yank_registers; +typedef struct yank_registers yank_registers_T; + +typedef size_t iter_register_T; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "ops.h.generated.h" #endif diff --git a/src/nvim/option.c b/src/nvim/option.c index 12c5889703..ba77d12a3d 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -3625,6 +3625,7 @@ static char *set_chars_option(win_T *wp, char_u **varp, bool set) struct chars_tab *tab; struct chars_tab fcs_tab[] = { + { &wp->w_p_fcs_chars.colorcol, "colorcol", NUL }, { &wp->w_p_fcs_chars.stl, "stl", ' ' }, { &wp->w_p_fcs_chars.stlnc, "stlnc", ' ' }, { &wp->w_p_fcs_chars.wbr, "wbr", ' ' }, @@ -6156,7 +6157,6 @@ static char_u *get_varp(vimoption_T *p) return (char_u *)&(curwin->w_p_cocu); case PV_COLE: return (char_u *)&(curwin->w_p_cole); - case PV_AI: return (char_u *)&(curbuf->b_p_ai); case PV_BIN: @@ -6197,6 +6197,8 @@ static char_u *get_varp(vimoption_T *p) return (char_u *)&(curbuf->b_p_cfu); case PV_OFU: return (char_u *)&(curbuf->b_p_ofu); + case PV_URF: + return (char_u *)&(curbuf->b_p_urf); case PV_EOL: return (char_u *)&(curbuf->b_p_eol); case PV_FIXEOL: @@ -6594,6 +6596,8 @@ void buf_copy_options(buf_T *buf, int flags) COPY_OPT_SCTX(buf, BV_CFU); buf->b_p_ofu = vim_strsave(p_ofu); COPY_OPT_SCTX(buf, BV_OFU); + buf->b_p_urf = vim_strsave(p_urf); + COPY_OPT_SCTX(buf, BV_URF); buf->b_p_tfu = vim_strsave(p_tfu); COPY_OPT_SCTX(buf, BV_TFU); buf->b_p_sts = p_sts; diff --git a/src/nvim/option_defs.h b/src/nvim/option_defs.h index 237288fbad..f3f7cf219e 100644 --- a/src/nvim/option_defs.h +++ b/src/nvim/option_defs.h @@ -753,6 +753,7 @@ EXTERN int p_wa; // 'writeany' EXTERN int p_wb; // 'writebackup' EXTERN long p_wd; // 'writedelay' EXTERN int p_cdh; // 'cdhome' +EXTERN char_u *p_urf; // 'userregister' EXTERN int p_force_on; ///< options that cannot be turned off. EXTERN int p_force_off; ///< options that cannot be turned on. @@ -849,6 +850,7 @@ enum { BV_WM, BV_VSTS, BV_VTS, + BV_URF, BV_COUNT, // must be the last one }; diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 2f941f5d0c..fd4661f9f9 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -2691,6 +2691,15 @@ return { defaults={if_true=4000} }, { + full_name='userregfun', abbreviation='urf', + type='string', scope={'buffer'}, + secure=true, + vi_def=true, + alloced=true, + varname='p_urf', + defaults={if_true=""} + }, + { full_name='varsofttabstop', abbreviation='vsts', short_desc=N_("list of numbers of spaces that <Tab> uses while editing"), type='string', list='comma', scope={'buffer'}, diff --git a/src/nvim/popupmnu.c b/src/nvim/popupmnu.c index 746429e5d5..4accddfce0 100644 --- a/src/nvim/popupmnu.c +++ b/src/nvim/popupmnu.c @@ -5,6 +5,8 @@ /// /// Popup menu (PUM) +#include "nvim/popupmnu.h" + #include <assert.h> #include <inttypes.h> #include <stdbool.h> @@ -16,6 +18,7 @@ #include "nvim/edit.h" #include "nvim/eval/typval.h" #include "nvim/ex_cmds.h" +#include "nvim/highlight.h" #include "nvim/insexpand.h" #include "nvim/memline.h" #include "nvim/memory.h" @@ -26,28 +29,29 @@ #include "nvim/screen.h" #include "nvim/search.h" #include "nvim/strings.h" +#include "nvim/syntax.h" #include "nvim/ui.h" #include "nvim/ui_compositor.h" #include "nvim/vim.h" #include "nvim/window.h" static pumitem_T *pum_array = NULL; // items of displayed pum -static int pum_size; // nr of items in "pum_array" -static int pum_selected; // index of selected item or -1 -static int pum_first = 0; // index of top item - -static int pum_height; // nr of displayed pum items -static int pum_width; // width of displayed pum items -static int pum_base_width; // width of pum items base -static int pum_kind_width; // width of pum items kind column -static int pum_extra_width; // width of extra stuff -static int pum_scrollbar; // one when scrollbar present, else zero -static bool pum_rl; // true when popupmenu is drawn 'rightleft' - -static int pum_anchor_grid; // grid where position is defined -static int pum_row; // top row of pum -static int pum_col; // left column of pum -static bool pum_above; // pum is drawn above cursor line +static int pum_size; // nr of items in "pum_array" +static int pum_selected; // index of selected item or -1 +static int pum_first = 0; // index of top item + +static int pum_height; // nr of displayed pum items +static int pum_width; // width of displayed pum items +static int pum_base_width; // width of pum items base +static int pum_kind_width; // width of pum items kind column +static int pum_extra_width; // width of extra stuff +static int pum_scrollbar; // one when scrollbar present, else zero +static bool pum_rl; // true when popupmenu is drawn 'rightleft' + +static int pum_anchor_grid; // grid where position is defined +static int pum_row; // top row of pum +static int pum_col; // left column of pum +static bool pum_above; // pum is drawn above cursor line static bool pum_is_visible = false; static bool pum_is_drawn = false; @@ -55,9 +59,18 @@ static bool pum_external = false; static bool pum_invalid = false; // the screen was just cleared #ifdef INCLUDE_GENERATED_DECLARATIONS -# include "popupmnu.c.generated.h" +#include "popupmnu.c.generated.h" #endif #define PUM_DEF_HEIGHT 10 +#define PUM_DEF_WIDTH 15 + +static int str_dispnsize(char *s, int len); +static int str_dispsize(char *s); + +// Forward declarations of needed syn functions. +int syn_id2attr(int hl_id); +char_u *syn_id2name(int id); +int syn_name2id(char_u*); static void pum_compute_size(void) { @@ -68,19 +81,19 @@ static void pum_compute_size(void) for (int i = 0; i < pum_size; i++) { int w; if (pum_array[i].pum_text != NULL) { - w = vim_strsize((char *)pum_array[i].pum_text); + w = str_dispsize((char*) pum_array[i].pum_text); if (pum_base_width < w) { pum_base_width = w; } } if (pum_array[i].pum_kind != NULL) { - w = vim_strsize((char *)pum_array[i].pum_kind) + 1; + w = str_dispsize((char*) pum_array[i].pum_kind) + 1; if (pum_kind_width < w) { pum_kind_width = w; } } if (pum_array[i].pum_extra != NULL) { - w = vim_strsize((char *)pum_array[i].pum_extra) + 1; + w = str_dispsize((char*) pum_array[i].pum_extra) + 1; if (pum_extra_width < w) { pum_extra_width = w; } @@ -176,7 +189,8 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i int def_width = (int)p_pw; win_T *pvwin = NULL; - FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { + FOR_ALL_WINDOWS_IN_TAB(wp, curtab) + { if (wp->w_p_pvw) { pvwin = wp; break; @@ -284,8 +298,7 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i def_width = max_width; } - if ((((cursor_col < Columns - p_pw) || (cursor_col < Columns - max_width)) - && !pum_rl) + if ((((cursor_col < Columns - p_pw) || (cursor_col < Columns - max_width)) && !pum_rl) || (pum_rl && ((cursor_col > p_pw) || (cursor_col > max_width)))) { // align pum with "cursor_col" pum_col = cursor_col; @@ -298,8 +311,7 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i pum_width = Columns - pum_col - pum_scrollbar; } - if ((pum_width > max_width + pum_kind_width + pum_extra_width + 1) - && (pum_width > p_pw)) { + if ((pum_width > max_width + pum_kind_width + pum_extra_width + 1) && (pum_width > p_pw)) { // the width is more than needed for the items, make it // narrower pum_width = max_width + pum_kind_width + pum_extra_width + 1; @@ -308,8 +320,7 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i pum_width = (int)p_pw; } } else if (((cursor_col > p_pw || cursor_col > max_width) && !pum_rl) - || (pum_rl && (cursor_col < Columns - p_pw - || cursor_col < Columns - max_width))) { + || (pum_rl && (cursor_col < Columns - p_pw || cursor_col < Columns - max_width))) { // align pum edge with "cursor_col" if (pum_rl && W_ENDCOL(curwin) < max_width + pum_scrollbar + 1) { pum_col = cursor_col + max_width + pum_scrollbar + 1; @@ -317,8 +328,7 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i pum_col = Columns - 1; } } else if (!pum_rl) { - if (curwin->w_wincol > Columns - max_width - pum_scrollbar - && max_width <= p_pw) { + if (curwin->w_wincol > Columns - max_width - pum_scrollbar && max_width <= p_pw) { // use full width to end of the screen pum_col = Columns - max_width - pum_scrollbar; if (pum_col < 0) { @@ -458,8 +468,7 @@ void pum_redraw(void) if (thumb_height == 0) { thumb_height = 1; } - thumb_pos = (pum_first * (pum_height - thumb_height) - + (pum_size - pum_height) / 2) + thumb_pos = (pum_first * (pum_height - thumb_height) + (pum_size - pum_height) / 2) / (pum_size - pum_height); } @@ -501,6 +510,7 @@ void pum_redraw(void) break; } + int cur_attr = attr; if (p != NULL) { for (;; MB_PTR_ADV(p)) { if (s == NULL) { @@ -508,7 +518,7 @@ void pum_redraw(void) } w = ptr2cells((char *)p); - if ((*p == NUL) || (*p == TAB) || (totwidth + w > pum_width)) { + if ((*p == NUL) || (*p == TAB) || (*p == '%' && *(p + 1) == '#') || (totwidth + w > pum_width)) { // Display the text that fits or comes before a Tab. // First convert it to printable characters. char_u *st; @@ -523,9 +533,9 @@ void pum_redraw(void) } if (pum_rl) { - char *rt = (char *)reverse_text(st); + char *rt = (char *) reverse_text(st); char *rt_start = rt; - int size = vim_strsize(rt); + int size = str_dispsize(rt); if (size > pum_width) { do { @@ -548,28 +558,47 @@ void pum_redraw(void) grid_col -= width; } else { // use grid_puts_len() to truncate the text - grid_puts(&pum_grid, st, row, grid_col, attr); + grid_puts(&pum_grid, st, row, grid_col, cur_attr); xfree(st); grid_col += width; } - if (*p != TAB) { - break; - } - // Display two spaces for a Tab. - if (pum_rl) { - grid_puts_len(&pum_grid, (char_u *)" ", 2, row, grid_col - 1, - attr); - grid_col -= 2; + if (*p == TAB) { + if (pum_rl) { + grid_puts_len(&pum_grid, (char_u *)" ", 2, row, grid_col - 1, attr); + grid_col -= 2; + } else { + grid_puts_len(&pum_grid, (char_u *)" ", 2, row, grid_col, attr); + grid_col += 2; + } + totwidth += 2; + // start text at next char + s = NULL; + width = 0; + } else if (*p == '%' && *(p + 1) == '#') { + // Parse highlights. Highlights are status-line-like. + char_u hl_attr[128]; + unsigned pi = 0; + p += 2; + while (*p != '#' && *p != '\0') { + if (pi < sizeof(hl_attr) - 1) { + hl_attr[pi++] = *p; + } + p ++; + } + hl_attr[pi] = 0; + if (*p == 0) + break; + cur_attr = + hl_combine_attr(attr, syn_id2attr(syn_name2id(hl_attr))); + + // Start the next char. + s = NULL; + width = 0; } else { - grid_puts_len(&pum_grid, (char_u *)" ", 2, row, grid_col, attr); - grid_col += 2; + break; } - totwidth += 2; - // start text at next char - s = NULL; - width = 0; } else { width += w; } @@ -583,11 +612,8 @@ void pum_redraw(void) } // Stop when there is nothing more to display. - if ((round == 3) - || ((round == 2) - && (pum_array[idx].pum_extra == NULL)) - || ((round == 1) - && (pum_array[idx].pum_kind == NULL) + if ((round == 3) || ((round == 2) && (pum_array[idx].pum_extra == NULL)) + || ((round == 1) && (pum_array[idx].pum_kind == NULL) && (pum_array[idx].pum_extra == NULL)) || (pum_base_width + n >= pum_width)) { break; @@ -616,12 +642,10 @@ void pum_redraw(void) if (pum_scrollbar > 0) { if (pum_rl) { grid_putchar(&pum_grid, ' ', row, col_off - pum_width, - i >= thumb_pos && i < thumb_pos + thumb_height - ? attr_thumb : attr_scroll); + i >= thumb_pos && i < thumb_pos + thumb_height ? attr_thumb : attr_scroll); } else { grid_putchar(&pum_grid, ' ', row, col_off + pum_width, - i >= thumb_pos && i < thumb_pos + thumb_height - ? attr_thumb : attr_scroll); + i >= thumb_pos && i < thumb_pos + thumb_height ? attr_thumb : attr_scroll); } } grid_puts_line_flush(false); @@ -934,8 +958,36 @@ void pum_set_event_info(dict_T *dict) (void)tv_dict_add_float(dict, S_LEN("row"), r); (void)tv_dict_add_float(dict, S_LEN("col"), c); (void)tv_dict_add_nr(dict, S_LEN("size"), pum_size); - (void)tv_dict_add_bool(dict, S_LEN("scrollbar"), - pum_scrollbar ? kBoolVarTrue : kBoolVarFalse); + (void)tv_dict_add_bool(dict, S_LEN("scrollbar"), pum_scrollbar ? kBoolVarTrue : kBoolVarFalse); +} + +static int str_dispnsize(char *s, int len) +{ + assert(s != NULL); + int size = 0; + int on = 1; + + while (*s != NUL && --len >= 0) { + if (*s == '%' && *(s + 1) == '#') { + on = 0; + s += 2; + len -= 2; + } else { + int l = (*utfc_ptr2len)(s); + if (on) + size += ptr2cells(s); + s += l; + len -= l - 1; + if (*s == '#') + on = 1; + } + } + return size; +} + +static int str_dispsize(char *s) +{ + return str_dispnsize(s, MAXCOL); } static void pum_position_at_mouse(int min_width) diff --git a/src/nvim/screen.c b/src/nvim/screen.c index 0d3b936c48..0b7b58dc40 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -3950,6 +3950,9 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc col_attr = cuc_attr; } else if (draw_color_col && VCOL_HLC == *color_cols) { col_attr = mc_attr; + // Draw the colorcolumn character. + c = wp->w_p_fcs_chars.colorcol; + schar_from_char(linebuf_char[off], c); } col_attr = hl_combine_attr(col_attr, line_attr); @@ -5495,13 +5498,61 @@ static void win_redr_border(win_T *wp) int *adj = wp->w_border_adj; int irow = wp->w_height_inner + wp->w_winbar_height, icol = wp->w_width_inner; + char* title = wp->w_float_config.title; + size_t n_title = wp->w_float_config.n_title; + stl_hlrec_t* title_hl = wp->w_float_config.title_hl; + + int m8[MAX_MCO + 1]; + int cc; + int len; + int t_attr = title_hl != NULL && title_hl->userhl + ? syn_id2attr(title_hl->userhl) + : 0; + t_attr = hl_combine_attr(attrs[1], t_attr); + + int title_pos = 2; + switch (wp->w_float_config.title_pos) { + case kTitleLeft: + title_pos = 2; + break; + case kTitleRight: + title_pos = icol - 2 - vim_strsize(title); + break; + case kTitleCenter: + title_pos = (icol - vim_strsize(title)) / 2 - 1; + break; + } + title_pos = title_pos < 2 ? 2 : title_pos; + if (adj[0]) { grid_puts_line_start(grid, 0); if (adj[3]) { grid_put_schar(grid, 0, 0, chars[0], attrs[0]); } for (int i = 0; i < icol; i++) { - grid_put_schar(grid, 0, i + adj[3], chars[1], attrs[1]); + schar_T ch; + int attr; + // Draw the title if in the correct position. + if (i > title_pos && n_title > 0 && i < icol - 2) { + cc = utfc_ptr2char((char_u*) title, m8); + len = utfc_ptr2len(title); + n_title -= len; + title += len; + + while (title_hl != NULL && + (title_hl + 1)->start != NULL && + (title_hl + 1)->start < title) { + ++ title_hl; + t_attr = hl_combine_attr(attrs[1], syn_id2attr(-title_hl->userhl)); + } + + schar_from_cc(ch, cc, m8); + attr = t_attr; + } else { + memcpy(ch, chars[1], sizeof(schar_T)); + attr = attrs[1]; + } + grid_put_schar(grid, 0, i + adj[3], ch, attr); } if (adj[1]) { grid_put_schar(grid, 0, icol + adj[3], chars[2], attrs[2]); diff --git a/src/nvim/shada.c b/src/nvim/shada.c index 6e80b550d8..cd3b967a9f 100644 --- a/src/nvim/shada.c +++ b/src/nvim/shada.c @@ -2377,7 +2377,7 @@ static inline void add_search_pattern(PossiblyFreedShadaEntry *const ret_pse, static inline void shada_initialize_registers(WriteMergerState *const wms, int max_reg_lines) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_ALWAYS_INLINE { - const void *reg_iter = NULL; + iter_register_T reg_iter = ITER_REGISTER_NULL; const bool limit_reg_lines = max_reg_lines >= 0; do { yankreg_T reg; @@ -2408,7 +2408,7 @@ static inline void shada_initialize_registers(WriteMergerState *const wms, int m } } }; - } while (reg_iter != NULL); + } while (reg_iter != ITER_REGISTER_NULL); } /// Replace numbered mark in WriteMergerState diff --git a/src/nvim/window.c b/src/nvim/window.c index c7f038850e..abb277bd23 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -3065,6 +3065,11 @@ static win_T *win_free_mem(win_T *win, int *dirp, tabpage_T *tp) wp = firstwin; } } + xfree(win->w_float_config.title); + xfree(win->w_float_config.title_hl); + win->w_float_config.title_hl = NULL; + win->w_float_config.title = NULL; + win->w_float_config.n_title = 0; win_free(win, tp); // When deleting the current window of another tab page select a new diff --git a/src/nvim/yankmap.c b/src/nvim/yankmap.c new file mode 100644 index 0000000000..d9229e015d --- /dev/null +++ b/src/nvim/yankmap.c @@ -0,0 +1,42 @@ +#include "nvim/yankmap.h" + +#include "nvim/memory.h" + +void init_yankmap(yankmap_T* map) +{ + memset(map, 0, sizeof(yankmap_T)); + + map_init(int, ptr_t, &map->reg_to_yankreg); + map_init(ptr_t, int, &map->yankreg_to_reg); +} + +yankreg_T* yankmap_get(yankmap_T* yankmap, int reg) +{ + yankreg_T** ret = + (yankreg_T**) map_ref(int, ptr_t)(&yankmap->reg_to_yankreg, reg, true); + + if (ret) { + if (! (*ret)) { + *ret = xcalloc(sizeof(yankreg_T), 1); + } + + /* Add the back-reference */ + int* ref = map_ref(ptr_t, int)(&yankmap->yankreg_to_reg, *ret, true); + *ref = reg; + + return *ret; + } + + return NULL; +} + +int yankmap_find(yankmap_T* yankmap, yankreg_T* yankreg) +{ + int* ref = map_ref(ptr_t, int)(&yankmap->yankreg_to_reg, yankreg, false); + + if (ref) { + return *ref; + } + + return -1; +} diff --git a/src/nvim/yankmap.h b/src/nvim/yankmap.h new file mode 100644 index 0000000000..da7c4dcf13 --- /dev/null +++ b/src/nvim/yankmap.h @@ -0,0 +1,24 @@ +#ifndef YANK_TRIE_H_ +#define YANK_TRIE_H_ + +#include <stdbool.h> +#include "nvim/ops.h" +#include "nvim/map.h" + +typedef struct { + /* Register name to yank register. */ + Map(int, ptr_t) reg_to_yankreg; + + /* Yank register to register name. */ + Map(ptr_t, int) yankreg_to_reg; +} yankmap_T; + +void init_yankmap(yankmap_T* yankmap); + +yankreg_T* yankmap_get(yankmap_T* yankmap, int index); + +yankreg_T* yankmap_put(yankmap_T* yankmap, int index); + +int yankmap_find(yankmap_T* yankmap, yankreg_T* yankreg); + +#endif diff --git a/test/functional/ui/highlight_spec.lua b/test/functional/ui/highlight_spec.lua index e065a727f3..946129b082 100644 --- a/test/functional/ui/highlight_spec.lua +++ b/test/functional/ui/highlight_spec.lua @@ -868,13 +868,14 @@ describe('CursorLine and CursorLineNr highlights', function() | ]]) + command('set fillchars+=colorcol:│') command('set colorcolumn=3') feed('i <esc>') screen:expect([[ - {1:{} {7: } | + {1:{} {7:│} | "{2:a}{7:"} : {3:abc} {3:// 10;} | - {1:}} {7: } | - {5: ^ }{7: }{5: }| + {1:}} {7:│} | + {5: ^ }{7:│}{5: }| | ]]) end) diff --git a/test/symbolic/klee/nvim/keymap.c b/test/symbolic/klee/nvim/keymap.c index 1f7f0e0911..26cdd9db47 100644 --- a/test/symbolic/klee/nvim/keymap.c +++ b/test/symbolic/klee/nvim/keymap.c @@ -223,6 +223,33 @@ static const struct key_name_entry { { K_F35, "F35" }, { K_F36, "F36" }, { K_F37, "F37" }, + { K_F38, "F38" }, + + { K_F39, "F39" }, + { K_F40, "F40" }, + { K_F41, "F41" }, + { K_F42, "F42" }, + { K_F43, "F43" }, + { K_F44, "F44" }, + { K_F45, "F45" }, + { K_F46, "F46" }, + { K_F47, "F47" }, + { K_F48, "F48" }, + { K_F49, "F49" }, + { K_F50, "F50" }, + { K_F51, "F51" }, + { K_F52, "F52" }, + { K_F53, "F53" }, + { K_F54, "F54" }, + { K_F55, "F55" }, + { K_F56, "F56" }, + { K_F57, "F57" }, + { K_F58, "F58" }, + { K_F59, "F59" }, + { K_F60, "F60" }, + { K_F61, "F61" }, + { K_F62, "F62" }, + { K_F63, "F63" }, { K_XF1, "xF1" }, { K_XF2, "xF2" }, |