diff options
-rw-r--r-- | man/nvim.1 | 7 | ||||
-rw-r--r-- | runtime/doc/helphelp.txt | 2 | ||||
-rwxr-xr-x | src/clint.py | 10 | ||||
-rw-r--r-- | src/nvim/eval.c | 80 | ||||
-rw-r--r-- | src/nvim/ex_cmds.c | 61 | ||||
-rw-r--r-- | src/nvim/file_search.c | 1 | ||||
-rw-r--r-- | src/nvim/fileio.c | 9 | ||||
-rw-r--r-- | src/nvim/globals.h | 1 | ||||
-rw-r--r-- | src/nvim/hardcopy.c | 4 | ||||
-rw-r--r-- | src/nvim/memory.c | 27 | ||||
-rw-r--r-- | src/nvim/message.c | 19 | ||||
-rw-r--r-- | src/nvim/misc1.c | 5 | ||||
-rw-r--r-- | src/nvim/normal.c | 30 | ||||
-rw-r--r-- | src/nvim/ops.c | 120 | ||||
-rw-r--r-- | src/nvim/os/env.c | 25 | ||||
-rw-r--r-- | src/nvim/os/os_defs.h | 2 | ||||
-rw-r--r-- | src/nvim/path.c | 21 | ||||
-rw-r--r-- | src/nvim/quickfix.c | 2 | ||||
-rw-r--r-- | src/nvim/shada.c | 2 | ||||
-rw-r--r-- | src/nvim/strings.c | 18 | ||||
-rw-r--r-- | src/nvim/syntax.c | 4 | ||||
-rw-r--r-- | src/nvim/tui/tui.c | 18 | ||||
-rw-r--r-- | src/nvim/version.c | 4 | ||||
-rw-r--r-- | src/nvim/vim.h | 1 | ||||
-rw-r--r-- | test/functional/core/job_spec.lua | 8 | ||||
-rw-r--r-- | test/functional/helpers.lua | 132 | ||||
-rw-r--r-- | test/functional/normal/put_spec.lua | 936 | ||||
-rw-r--r-- | test/functional/ui/output_spec.lua | 5 | ||||
-rw-r--r-- | test/functional/ui/screen.lua | 13 | ||||
-rw-r--r-- | test/helpers.lua | 20 | ||||
-rw-r--r-- | test/unit/os/env_spec.lua | 67 |
31 files changed, 1363 insertions, 291 deletions
diff --git a/man/nvim.1 b/man/nvim.1 index 70bf480f2b..98d97c2d5a 100644 --- a/man/nvim.1 +++ b/man/nvim.1 @@ -372,9 +372,10 @@ Used to set the 'shell' option, which determines the shell used by the .Ic :terminal command. .It Ev NVIM_TUI_ENABLE_CURSOR_SHAPE -If defined, change the cursor shape to a vertical bar while in insert mode. -Requires that the host terminal supports the DECSCUSR CSI escape sequence. -Has no effect in GUIs. +Set to 0 to prevent Nvim from changing the cursor shape. +Set to 1 to enable non-blinking mode-sensitive cursor (this is the default). +Set to 2 to enable blinking mode-sensitive cursor. +Host terminal must support the DECSCUSR CSI escape sequence. .Pp Depending on the terminal emulator, using this option with .Nm diff --git a/runtime/doc/helphelp.txt b/runtime/doc/helphelp.txt index ca341af200..ad1611133a 100644 --- a/runtime/doc/helphelp.txt +++ b/runtime/doc/helphelp.txt @@ -185,7 +185,7 @@ command: > < *:helpt* *:helptags* - *E154* *E150* *E151* *E152* *E153* *E670* + *E154* *E150* *E151* *E152* *E153* *E670* *E856* :helpt[ags] [++t] {dir} Generate the help tags file(s) for directory {dir}. When {dir} is ALL then all "doc" directories in diff --git a/src/clint.py b/src/clint.py index 0c9f55c71e..df71282362 100755 --- a/src/clint.py +++ b/src/clint.py @@ -3166,11 +3166,15 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension, # Check if some verboten C functions are being used. if Search(r'\bsprintf\b', line): error(filename, linenum, 'runtime/printf', 5, - 'Never use sprintf. Use snprintf instead.') - match = Search(r'\b(strcpy|strcat)\b', line) + 'Use snprintf instead of sprintf.') + match = Search(r'\b(STRCPY|strcpy)\b', line) if match: error(filename, linenum, 'runtime/printf', 4, - 'Almost always, snprintf is better than %s' % match.group(1)) + 'Use xstrlcpy or snprintf instead of %s' % match.group(1)) + match = Search(r'\b(STRNCAT|strncat)\b', line) + if match: + error(filename, linenum, 'runtime/printf', 4, + 'Use xstrlcat instead of %s' % match.group(1)) # Check for suspicious usage of "if" like # } if (a == b) { diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 6b14d21da7..dffbbe9df9 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -9748,60 +9748,54 @@ static void f_function(typval_T *argvars, typval_T *rettv, FunPtr fptr) } } if (dict_idx > 0 || arg_idx > 0 || arg_pt != NULL) { - partial_T *pt = (partial_T *)xcalloc(1, sizeof(partial_T)); + partial_T *const pt = xcalloc(1, sizeof(*pt)); // result is a VAR_PARTIAL - if (pt != NULL) { - if (arg_idx > 0 || (arg_pt != NULL && arg_pt->pt_argc > 0)) { - listitem_T *li; + 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 = (list == NULL ? 0 : list->lv_len); + + pt->pt_argc = arg_len + lv_len; + pt->pt_argv = xmalloc(sizeof(pt->pt_argv[0]) * pt->pt_argc); + if (pt->pt_argv == NULL) { + xfree(pt); + xfree(name); + return; + } else { int i = 0; - int arg_len = 0; - int lv_len = 0; - - if (arg_pt != NULL) { - arg_len = arg_pt->pt_argc; + for (; i < arg_len; i++) { + copy_tv(&arg_pt->pt_argv[i], &pt->pt_argv[i]); } - if (list != NULL) { - lv_len = list->lv_len; - } - pt->pt_argc = arg_len + lv_len; - pt->pt_argv = (typval_T *)xmalloc(sizeof(typval_T) * pt->pt_argc); - if (pt->pt_argv == NULL) { - xfree(pt); - xfree(name); - return; - } else { - for (i = 0; i < arg_len; i++) { - copy_tv(&arg_pt->pt_argv[i], &pt->pt_argv[i]); - } - if (lv_len > 0) { - for (li = list->lv_first; li != NULL; li = li->li_next) { - copy_tv(&li->li_tv, &pt->pt_argv[i++]); - } + if (lv_len > 0) { + for (listitem_T *li = list->lv_first; + li != NULL; + li = li->li_next) { + copy_tv(&li->li_tv, &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; + // 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)++; - } 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; - pt->pt_name = name; - func_ref(pt->pt_name); } + + pt->pt_refcount = 1; + pt->pt_name = name; + func_ref(pt->pt_name); + rettv->v_type = VAR_PARTIAL; rettv->vval.v_partial = pt; } else { diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 69eed33736..56919db024 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -1408,32 +1408,30 @@ char_u *make_filter_cmd(char_u *cmd, char_u *itmp, char_u *otmp) } if (itmp != NULL) { - strncat(buf, " < ", len); - strncat(buf, (char *) itmp, len); + xstrlcat(buf, " < ", len - 1); + xstrlcat(buf, (const char *)itmp, len - 1); } #else // For shells that don't understand braces around commands, at least allow // the use of commands in a pipe. strncpy(buf, cmd, len); if (itmp != NULL) { - char_u *p; - // If there is a pipe, we have to put the '<' in front of it. // Don't do this when 'shellquote' is not empty, otherwise the // redirection would be inside the quotes. if (*p_shq == NUL) { - p = strchr(buf, '|'); + char *const p = strchr(buf, '|'); if (p != NULL) { *p = NUL; } } - strncat(buf, " < ", len); - strncat(buf, (char *) itmp, len); + xstrlcat(buf, " < ", len); + xstrlcat(buf, (const char *)itmp, len); if (*p_shq == NUL) { - p = strchr(cmd, '|'); + const char *const p = strchr((const char *)cmd, '|'); if (p != NULL) { - strncat(buf, " ", len); // Insert a space before the '|' for DOS - strncat(buf, p, len); + xstrlcat(buf, " ", len - 1); // Insert a space before the '|' for DOS + xstrlcat(buf, p, len - 1); } } } @@ -4814,9 +4812,13 @@ void fix_help_buffer(void) vimconv_T vc; char_u *cp; - /* Find all "doc/ *.txt" files in this directory. */ - add_pathsep((char *)NameBuff); - STRCAT(NameBuff, "doc/*.??[tx]"); + // Find all "doc/ *.txt" files in this directory. + if (!add_pathsep((char *)NameBuff) + || STRLCAT(NameBuff, "doc/*.??[tx]", + sizeof(NameBuff)) >= MAXPATHL) { + EMSG(_(e_fnametoolong)); + continue; + } // Note: We cannot just do `&NameBuff` because it is a statically sized array // so `NameBuff == &NameBuff` according to C semantics. @@ -4979,8 +4981,12 @@ static void helptags_one(char_u *dir, char_u *ext, char_u *tagfname, // Find all *.txt files. size_t dirlen = STRLCPY(NameBuff, dir, sizeof(NameBuff)); - STRCAT(NameBuff, "/**/*"); // NOLINT - STRCAT(NameBuff, ext); + if (dirlen >= MAXPATHL + || STRLCAT(NameBuff, "/**/*", sizeof(NameBuff)) >= MAXPATHL // NOLINT + || STRLCAT(NameBuff, ext, sizeof(NameBuff)) >= MAXPATHL) { + EMSG(_(e_fnametoolong)); + return; + } // Note: We cannot just do `&NameBuff` because it is a statically sized array // so `NameBuff == &NameBuff` according to C semantics. @@ -4993,13 +4999,16 @@ static void helptags_one(char_u *dir, char_u *ext, char_u *tagfname, return; } - /* - * Open the tags file for writing. - * Do this before scanning through all the files. - */ - STRLCPY(NameBuff, dir, sizeof(NameBuff)); - add_pathsep((char *)NameBuff); - STRNCAT(NameBuff, tagfname, sizeof(NameBuff) - dirlen - 2); + // + // Open the tags file for writing. + // Do this before scanning through all the files. + // + memcpy(NameBuff, dir, dirlen + 1); + if (!add_pathsep((char *)NameBuff) + || STRLCAT(NameBuff, tagfname, sizeof(NameBuff)) >= MAXPATHL) { + EMSG(_(e_fnametoolong)); + return; + } fd_tags = mch_fopen((char *)NameBuff, "w"); if (fd_tags == NULL) { EMSG2(_("E152: Cannot open %s for writing"), NameBuff); @@ -5173,8 +5182,12 @@ static void do_helptags(char_u *dirname, bool add_help_tags) // Get a list of all files in the help directory and in subdirectories. STRLCPY(NameBuff, dirname, sizeof(NameBuff)); - add_pathsep((char *)NameBuff); - STRCAT(NameBuff, "**"); + if (!add_pathsep((char *)NameBuff) + || STRLCAT(NameBuff, "**", sizeof(NameBuff)) >= MAXPATHL) { + EMSG(_(e_fnametoolong)); + xfree(dirname); + return; + } // Note: We cannot just do `&NameBuff` because it is a statically sized array // so `NameBuff == &NameBuff` according to C semantics. diff --git a/src/nvim/file_search.c b/src/nvim/file_search.c index 03cb504f17..73faac0a43 100644 --- a/src/nvim/file_search.c +++ b/src/nvim/file_search.c @@ -193,7 +193,6 @@ typedef struct ff_search_ctx_T { static char_u e_pathtoolong[] = N_("E854: path too long for completion"); - /* * Initialization routine for vim_findfile(). * diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index 13329d771b..d433afab3e 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -1882,6 +1882,7 @@ failed: xfree(keep_msg); keep_msg = NULL; + p = NULL; msg_scrolled_ign = TRUE; if (!read_stdin && !read_buffer) { @@ -4981,8 +4982,8 @@ buf_check_timestamp ( set_vim_var_string(VV_WARNINGMSG, tbuf, -1); if (can_reload) { if (*mesg2 != NUL) { - strncat(tbuf, "\n", tbuf_len); - strncat(tbuf, mesg2, tbuf_len); + xstrlcat(tbuf, "\n", tbuf_len - 1); + xstrlcat(tbuf, mesg2, tbuf_len - 1); } if (do_dialog(VIM_WARNING, (char_u *) _("Warning"), (char_u *) tbuf, (char_u *) _("&OK\n&Load File"), 1, NULL, true) == 2) { @@ -4990,8 +4991,8 @@ buf_check_timestamp ( } } else if (State > NORMAL_BUSY || (State & CMDLINE) || already_warned) { if (*mesg2 != NUL) { - strncat(tbuf, "; ", tbuf_len); - strncat(tbuf, mesg2, tbuf_len); + xstrlcat(tbuf, "; ", tbuf_len - 1); + xstrlcat(tbuf, mesg2, tbuf_len - 1); } EMSG(tbuf); retval = 2; diff --git a/src/nvim/globals.h b/src/nvim/globals.h index e3c84cb852..baa85c01f8 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -1215,6 +1215,7 @@ EXTERN char_u e_invalidreg[] INIT(= N_("E850: Invalid register name")); EXTERN char_u e_dirnotf[] INIT(= N_( "E919: Directory not found in '%s': \"%s\"")); EXTERN char_u e_unsupportedoption[] INIT(= N_("E519: Option not supported")); +EXTERN char_u e_fnametoolong[] INIT(= N_("E856: Filename too long")); EXTERN char top_bot_msg[] INIT(= N_("search hit TOP, continuing at BOTTOM")); diff --git a/src/nvim/hardcopy.c b/src/nvim/hardcopy.c index cb0415a486..c2dc6231f1 100644 --- a/src/nvim/hardcopy.c +++ b/src/nvim/hardcopy.c @@ -1544,8 +1544,8 @@ static int prt_find_resource(char *name, struct prt_ps_resource_S *resource) /* Look for named resource file in runtimepath */ STRCPY(buffer, "print"); add_pathsep((char *)buffer); - vim_strcat(buffer, (char_u *)name, MAXPATHL); - vim_strcat(buffer, (char_u *)".ps", MAXPATHL); + xstrlcat((char *)buffer, name, MAXPATHL); + xstrlcat((char *)buffer, ".ps", MAXPATHL); resource->filename[0] = NUL; retval = (do_in_runtimepath(buffer, 0, prt_resource_name, resource->filename) && resource->filename[0] != NUL); diff --git a/src/nvim/memory.c b/src/nvim/memory.c index 92ead873ae..6408ac1664 100644 --- a/src/nvim/memory.c +++ b/src/nvim/memory.c @@ -389,6 +389,33 @@ size_t xstrlcpy(char *restrict dst, const char *restrict src, size_t size) return ret; } +/// xstrlcat - Appends string src to the end of dst. +/// +/// Compatible with *BSD strlcat: Appends at most (dstsize - strlen(dst) - 1) +/// characters. dst will be NUL-terminated. +/// +/// Note: Replaces `vim_strcat`. +/// +/// @param dst Where to copy the string to +/// @param src Where to copy the string from +/// @param dstsize Size of destination buffer, must be greater than 0 +/// @return strlen(src) + MIN(dstsize, strlen(initial dst)). +/// If retval >= dstsize, truncation occurs. +size_t xstrlcat(char *restrict dst, const char *restrict src, size_t dstsize) + FUNC_ATTR_NONNULL_ALL +{ + assert(dstsize > 0); + size_t srclen = strlen(src); + size_t dstlen = strlen(dst); + size_t ret = srclen + dstlen; // Total string length (excludes NUL) + if (srclen) { + size_t len = (ret >= dstsize) ? dstsize - 1 : ret; + memcpy(dst + dstlen, src, len - dstlen); + dst[len] = '\0'; + } + return ret; // Does not include NUL. +} + /// strdup() wrapper /// /// @see {xmalloc} diff --git a/src/nvim/message.c b/src/nvim/message.c index 749fa8a706..91dd042777 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -381,20 +381,17 @@ static int other_sourcing_name(void) return FALSE; } -/* - * Get the message about the source, as used for an error message. - * Returns an allocated string with room for one more character. - * Returns NULL when no message is to be given. - */ +/// Get the message about the source, as used for an error message. +/// Returns an allocated string with room for one more character. +/// Returns NULL when no message is to be given. static char_u *get_emsg_source(void) { - char_u *Buf, *p; - if (sourcing_name != NULL && other_sourcing_name()) { - p = (char_u *)_("Error detected while processing %s:"); - Buf = xmalloc(STRLEN(sourcing_name) + STRLEN(p)); - sprintf((char *)Buf, (char *)p, sourcing_name); - return Buf; + char_u *p = (char_u *)_("Error detected while processing %s:"); + size_t len = STRLEN(sourcing_name) + STRLEN(p) + 1; + char_u *buf = xmalloc(len); + snprintf((char *)buf, len, (char *)p, sourcing_name); + return buf; } return NULL; } diff --git a/src/nvim/misc1.c b/src/nvim/misc1.c index 71aa6e83e5..ba26381e23 100644 --- a/src/nvim/misc1.c +++ b/src/nvim/misc1.c @@ -2507,8 +2507,9 @@ void msgmore(long n) vim_snprintf((char *)msg_buf, MSG_BUF_LEN, _("%" PRId64 " fewer lines"), (int64_t)pn); } - if (got_int) - vim_strcat(msg_buf, (char_u *)_(" (Interrupted)"), MSG_BUF_LEN); + if (got_int) { + xstrlcat((char *)msg_buf, _(" (Interrupted)"), MSG_BUF_LEN); + } if (msg(msg_buf)) { set_keep_msg(msg_buf, 0); keep_msg_more = TRUE; diff --git a/src/nvim/normal.c b/src/nvim/normal.c index 227bfbe779..b17b4c584e 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -1130,6 +1130,7 @@ static int normal_execute(VimState *state, int key) start_selection(); unshift_special(&s->ca); s->idx = find_command(s->ca.cmdchar); + assert(s->idx >= 0); } else if ((nv_cmds[s->idx].cmd_flags & NV_SSS) && (mod_mask & MOD_MASK_SHIFT)) { start_selection(); @@ -1517,10 +1518,7 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank) coladvance(curwin->w_curswant); } cap->count0 = redo_VIsual_count; - if (redo_VIsual_count != 0) - cap->count1 = redo_VIsual_count; - else - cap->count1 = 1; + cap->count1 = (cap->count0 == 0 ? 1 : cap->count0); } else if (VIsual_active) { if (!gui_yank) { /* Save the current VIsual area for '< and '> marks, and "gv" */ @@ -7727,16 +7725,22 @@ static void nv_put(cmdarg_T *cap) savereg = copy_register(regname); } - /* Now delete the selected text. */ - cap->cmdchar = 'd'; - cap->nchar = NUL; - cap->oap->regname = NUL; - nv_operator(cap); - do_pending_operator(cap, 0, false); - empty = (curbuf->b_ml.ml_flags & ML_EMPTY); + // To place the cursor correctly after a blockwise put, and to leave the + // text in the correct position when putting over a selection with + // 'virtualedit' and past the end of the line, we use the 'c' operator in + // do_put(), which requires the visual selection to still be active. + if (!VIsual_active || VIsual_mode == 'V' || regname != '.') { + // Now delete the selected text. + cap->cmdchar = 'd'; + cap->nchar = NUL; + cap->oap->regname = NUL; + nv_operator(cap); + do_pending_operator(cap, 0, false); + empty = (curbuf->b_ml.ml_flags & ML_EMPTY); - /* delete PUT_LINE_BACKWARD; */ - cap->oap->regname = regname; + // delete PUT_LINE_BACKWARD; + cap->oap->regname = regname; + } /* When deleted a linewise Visual area, put the register as * lines to avoid it joined with the next line. When deletion was diff --git a/src/nvim/ops.c b/src/nvim/ops.c index 10d6be85f8..9a01891483 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -2637,12 +2637,79 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) * special characters (newlines, etc.). */ if (regname == '.') { - (void)stuff_inserted((dir == FORWARD ? (count == -1 ? 'o' : 'a') : - (count == -1 ? 'O' : 'i')), count, FALSE); - /* Putting the text is done later, so can't really move the cursor to - * the next character. Use "l" to simulate it. */ - if ((flags & PUT_CURSEND) && gchar_cursor() != NUL) - stuffcharReadbuff('l'); + bool non_linewise_vis = (VIsual_active && VIsual_mode != 'V'); + + // PUT_LINE has special handling below which means we use 'i' to start. + char command_start_char = non_linewise_vis ? 'c' : + (flags & PUT_LINE ? 'i' : (dir == FORWARD ? 'a' : 'i')); + + // To avoid 'autoindent' on linewise puts, create a new line with `:put _`. + if (flags & PUT_LINE) { + do_put('_', NULL, dir, 1, PUT_LINE); + } + + // If given a count when putting linewise, we stuff the readbuf with the + // dot register 'count' times split by newlines. + if (flags & PUT_LINE) { + stuffcharReadbuff(command_start_char); + for (; count > 0; count--) { + (void)stuff_inserted(NUL, 1, count != 1); + if (count != 1) { + // To avoid 'autoindent' affecting the text, use Ctrl_U to remove any + // whitespace. Can't just insert Ctrl_U into readbuf1, this would go + // back to the previous line in the case of 'noautoindent' and + // 'backspace' includes "eol". So we insert a dummy space for Ctrl_U + // to consume. + stuffReadbuff((char_u *)"\n "); + stuffcharReadbuff(Ctrl_U); + } + } + } else { + (void)stuff_inserted(command_start_char, count, false); + } + + // Putting the text is done later, so can't move the cursor to the next + // character. Simulate it with motion commands after the insert. + if (flags & PUT_CURSEND) { + if (flags & PUT_LINE) { + stuffReadbuff((char_u *)"j0"); + } else { + // Avoid ringing the bell from attempting to move into the space after + // the current line. We can stuff the readbuffer with "l" if: + // 1) 'virtualedit' is "all" or "onemore" + // 2) We are not at the end of the line + // 3) We are not (one past the end of the line && on the last line) + // This allows a visual put over a selection one past the end of the + // line joining the current line with the one below. + + // curwin->w_cursor.col marks the byte position of the cursor in the + // currunt line. It increases up to a max of + // STRLEN(ml_get(curwin->w_cursor.lnum)). With 'virtualedit' and the + // cursor past the end of the line, curwin->w_cursor.coladd is + // incremented instead of curwin->w_cursor.col. + char_u *cursor_pos = get_cursor_pos_ptr(); + bool one_past_line = (*cursor_pos == NUL); + bool eol = false; + if (!one_past_line) { + eol = (*(cursor_pos + mb_ptr2len(cursor_pos)) == NUL); + } + + bool ve_allows = (ve_flags == VE_ALL || ve_flags == VE_ONEMORE); + bool eof = curbuf->b_ml.ml_line_count == curwin->w_cursor.lnum + && one_past_line; + if (ve_allows || !(eol || eof)) { + stuffcharReadbuff('l'); + } + } + } else if (flags & PUT_LINE) { + stuffReadbuff((char_u *)"g'["); + } + + // So the 'u' command restores cursor position after ".p, save the cursor + // position now (though not saving any text). + if (command_start_char == 'a') { + u_save(curwin->w_cursor.lnum, curwin->w_cursor.lnum + 1); + } return; } @@ -2831,14 +2898,12 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) else getvcol(curwin, &curwin->w_cursor, NULL, NULL, &col); - if (has_mbyte) - /* move to start of next multi-byte character */ - curwin->w_cursor.col += (*mb_ptr2len)(get_cursor_pos_ptr()); - else if (c != TAB || ve_flags != VE_ALL) - ++curwin->w_cursor.col; - ++col; - } else + // move to start of next multi-byte character + curwin->w_cursor.col += (*mb_ptr2len)(get_cursor_pos_ptr()); + col++; + } else { getvcol(curwin, &curwin->w_cursor, &col, NULL, &endcol2); + } col += curwin->w_cursor.coladd; if (ve_flags == VE_ALL @@ -2892,8 +2957,7 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) bd.startspaces = incr - bd.endspaces; --bd.textcol; delcount = 1; - if (has_mbyte) - bd.textcol -= (*mb_head_off)(oldp, oldp + bd.textcol); + bd.textcol -= (*mb_head_off)(oldp, oldp + bd.textcol); if (oldp[bd.textcol] != TAB) { /* Only a Tab can be split into spaces. Other * characters will have to be moved to after the @@ -2975,21 +3039,13 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) // if type is kMTCharWise, FORWARD is the same as BACKWARD on the next // char if (dir == FORWARD && gchar_cursor() != NUL) { - if (has_mbyte) { - int bytelen = (*mb_ptr2len)(get_cursor_pos_ptr()); - - /* put it on the next of the multi-byte character. */ - col += bytelen; - if (yanklen) { - curwin->w_cursor.col += bytelen; - curbuf->b_op_end.col += bytelen; - } - } else { - ++col; - if (yanklen) { - ++curwin->w_cursor.col; - ++curbuf->b_op_end.col; - } + int bytelen = (*mb_ptr2len)(get_cursor_pos_ptr()); + + // put it on the next of the multi-byte character. + col += bytelen; + if (yanklen) { + curwin->w_cursor.col += bytelen; + curbuf->b_op_end.col += bytelen; } } curbuf->b_op_start = curwin->w_cursor; @@ -3027,7 +3083,9 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) } if (VIsual_active) lnum++; - } while (VIsual_active && lnum <= curbuf->b_visual.vi_end.lnum); + } while (VIsual_active + && (lnum <= curbuf->b_visual.vi_end.lnum + || lnum <= curbuf->b_visual.vi_start.lnum)); if (VIsual_active) { /* reset lnum to the last visual line */ lnum--; diff --git a/src/nvim/os/env.c b/src/nvim/os/env.c index 47d541c6a7..109b5c7175 100644 --- a/src/nvim/os/env.c +++ b/src/nvim/os/env.c @@ -226,25 +226,24 @@ char_u *expand_env_save_opt(char_u *src, bool one) /// "~/" is also expanded, using $HOME. For Unix "~user/" is expanded. /// Skips over "\ ", "\~" and "\$" (not for Win32 though). /// If anything fails no expansion is done and dst equals src. -/// @param src Input string e.g. "$HOME/vim.hlp" -/// @param dst Where to put the result -/// @param dstlen Maximum length of the result +/// +/// @param src Input string e.g. "$HOME/vim.hlp" +/// @param dst[out] Where to put the result +/// @param dstlen Maximum length of the result void expand_env(char_u *src, char_u *dst, int dstlen) { expand_env_esc(src, dst, dstlen, false, false, NULL); } /// Expand environment variable with path name and escaping. -/// "~/" is also expanded, using $HOME. For Unix "~user/" is expanded. -/// Skips over "\ ", "\~" and "\$" (not for Win32 though). -/// If anything fails no expansion is done and dst equals src. -/// prefix recognize the start of a new name, for '~' expansion. -/// @param srcp Input string e.g. "$HOME/vim.hlp" -/// @param dst Where to put the result -/// @param dstlen Maximum length of the result -/// @param esc Should we escape spaces in expanded variables? -/// @param one Should we expand more than one '~'? -/// @param prefix Common prefix for paths, can be NULL +/// @see expand_env +/// +/// @param srcp Input string e.g. "$HOME/vim.hlp" +/// @param dst[out] Where to put the result +/// @param dstlen Maximum length of the result +/// @param esc Escape spaces in expanded variables +/// @param one `srcp` is a single filename +/// @param prefix Start again after this (can be NULL) void expand_env_esc(char_u *restrict srcp, char_u *restrict dst, int dstlen, diff --git a/src/nvim/os/os_defs.h b/src/nvim/os/os_defs.h index 5e164b54a5..14c210c69c 100644 --- a/src/nvim/os/os_defs.h +++ b/src/nvim/os/os_defs.h @@ -16,7 +16,7 @@ #define BASENAMELEN (NAME_MAX - 5) // Use the system path length if it makes sense. -#if defined(PATH_MAX) && (PATH_MAX > 1000) +#if defined(PATH_MAX) && (PATH_MAX > 1024) # define MAXPATHL PATH_MAX #else # define MAXPATHL 1024 diff --git a/src/nvim/path.c b/src/nvim/path.c index 3d1def8dd4..7e1183d5db 100644 --- a/src/nvim/path.c +++ b/src/nvim/path.c @@ -391,15 +391,22 @@ char *concat_fnames_realloc(char *fname1, const char *fname2, bool sep) fname2, len2, sep); } -/* - * Add a path separator to a file name, unless it already ends in a path - * separator. - */ -void add_pathsep(char *p) +/// Adds a path separator to a filename, unless it already ends in one. +/// +/// @return `true` if the path separator was added or already existed. +/// `false` if the filename is too long. +bool add_pathsep(char *p) FUNC_ATTR_NONNULL_ALL { - if (*p != NUL && !after_pathsep(p, p + strlen(p))) - strcat(p, PATHSEPSTR); + const size_t len = strlen(p); + if (*p != NUL && !after_pathsep(p, p + len)) { + const size_t pathsep_len = sizeof(PATHSEPSTR); + if (len > MAXPATHL - pathsep_len) { + return false; + } + memcpy(p + len, PATHSEPSTR, pathsep_len); + } + return true; } /// Get an allocated copy of the full path to a file. diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c index 19287ecffb..cebff5e8b7 100644 --- a/src/nvim/quickfix.c +++ b/src/nvim/quickfix.c @@ -2126,7 +2126,7 @@ static void qf_msg(qf_info_T *qi, int which, char *lead) memset(buf + len, ' ', 34 - len); buf[34] = NUL; } - vim_strcat(buf, (char_u *)title, IOSIZE); + xstrlcat((char *)buf, title, IOSIZE); } trunc_string(buf, buf, (int)Columns - 1, IOSIZE); msg(buf); diff --git a/src/nvim/shada.c b/src/nvim/shada.c index 8cf5976e8b..197b029591 100644 --- a/src/nvim/shada.c +++ b/src/nvim/shada.c @@ -1986,7 +1986,7 @@ static ShaDaWriteResult shada_pack_encoded_entry(msgpack_packer *const packer, entry.data.data.reg.contents = xmemdup(entry.data.data.reg.contents, (entry.data.data.reg.contents_size - * sizeof(entry.data.data.reg.contents))); + * sizeof(entry.data.data.reg.contents[0]))); for (size_t i = 0; i < entry.data.data.reg.contents_size; i++) { if (i >= first_non_ascii) { entry.data.data.reg.contents[i] = get_converted_string( diff --git a/src/nvim/strings.c b/src/nvim/strings.c index c1800a0639..5b4f23f30e 100644 --- a/src/nvim/strings.c +++ b/src/nvim/strings.c @@ -344,24 +344,6 @@ void del_trailing_spaces(char_u *ptr) *q = NUL; } -/* - * Like strcat(), but make sure the result fits in "tosize" bytes and is - * always NUL terminated. - */ -void vim_strcat(char_u *restrict to, const char_u *restrict from, - size_t tosize) - FUNC_ATTR_NONNULL_ALL -{ - size_t tolen = STRLEN(to); - size_t fromlen = STRLEN(from); - - if (tolen + fromlen + 1 > tosize) { - memcpy(to + tolen, from, tosize - tolen - 1); - to[tosize - 1] = NUL; - } else - STRCPY(to + tolen, from); -} - #if (!defined(HAVE_STRCASECMP) && !defined(HAVE_STRICMP)) /* * Compare two strings, ignoring case, using current locale. diff --git a/src/nvim/syntax.c b/src/nvim/syntax.c index aded08faee..5bfc655645 100644 --- a/src/nvim/syntax.c +++ b/src/nvim/syntax.c @@ -6902,8 +6902,8 @@ static int highlight_list_arg(int id, int didh, int type, int iarg, char_u *sarg for (i = 0; hl_attr_table[i] != 0; ++i) { if (iarg & hl_attr_table[i]) { if (buf[0] != NUL) - vim_strcat(buf, (char_u *)",", 100); - vim_strcat(buf, (char_u *)hl_name_table[i], 100); + xstrlcat((char *)buf, ",", 100); + xstrlcat((char *)buf, hl_name_table[i], 100); iarg &= ~hl_attr_table[i]; /* don't want "inverse" */ } } diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 74187e07c0..342c68818d 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -844,7 +844,7 @@ static void fix_terminfo(TUIData *data) } if (STARTS_WITH(term, "xterm") || STARTS_WITH(term, "rxvt")) { - unibi_set_if_empty(ut, unibi_cursor_normal, "\x1b[?12l\x1b[?25h"); + unibi_set_if_empty(ut, unibi_cursor_normal, "\x1b[?25h"); unibi_set_if_empty(ut, unibi_cursor_invisible, "\x1b[?25l"); unibi_set_if_empty(ut, unibi_flash_screen, "\x1b[?5h$<100/>\x1b[?5l"); unibi_set_if_empty(ut, unibi_exit_attribute_mode, "\x1b(B\x1b[m"); @@ -877,9 +877,11 @@ static void fix_terminfo(TUIData *data) unibi_set_str(ut, unibi_set_a_background, XTERM_SETAB); } - if (os_getenv("NVIM_TUI_ENABLE_CURSOR_SHAPE") == NULL) { + const char * env_cusr_shape = os_getenv("NVIM_TUI_ENABLE_CURSOR_SHAPE"); + if (env_cusr_shape && strncmp(env_cusr_shape, "0", 1) == 0) { goto end; } + bool cusr_blink = env_cusr_shape && strncmp(env_cusr_shape, "2", 1) == 0; #define TMUX_WRAP(seq) (inside_tmux ? "\x1bPtmux;\x1b" seq "\x1b\\" : seq) // Support changing cursor shape on some popular terminals. @@ -891,22 +893,22 @@ static void fix_terminfo(TUIData *data) // Konsole uses a proprietary escape code to set the cursor shape // and does not support DECSCUSR. data->unibi_ext.set_cursor_shape_bar = (int)unibi_add_ext_str(ut, NULL, - TMUX_WRAP("\x1b]50;CursorShape=1;BlinkingCursorEnabled=1\x07")); + TMUX_WRAP("\x1b]50;CursorShape=1\x07")); data->unibi_ext.set_cursor_shape_ul = (int)unibi_add_ext_str(ut, NULL, - TMUX_WRAP("\x1b]50;CursorShape=2;BlinkingCursorEnabled=1\x07")); + TMUX_WRAP("\x1b]50;CursorShape=2\x07")); data->unibi_ext.set_cursor_shape_block = (int)unibi_add_ext_str(ut, NULL, - TMUX_WRAP("\x1b]50;CursorShape=0;BlinkingCursorEnabled=0\x07")); + TMUX_WRAP("\x1b]50;CursorShape=0\x07")); } else if (!vte_version || atoi(vte_version) >= 3900) { // Assume that the terminal supports DECSCUSR unless it is an // old VTE based terminal. This should not get wrapped for tmux, // which will handle it via its Ss/Se terminfo extension - usually // according to its terminal-overrides. data->unibi_ext.set_cursor_shape_bar = - (int)unibi_add_ext_str(ut, NULL, "\x1b[5 q"); + (int)unibi_add_ext_str(ut, NULL, cusr_blink ? "\x1b[5 q" : "\x1b[6 q"); data->unibi_ext.set_cursor_shape_ul = - (int)unibi_add_ext_str(ut, NULL, "\x1b[3 q"); + (int)unibi_add_ext_str(ut, NULL, cusr_blink ? "\x1b[3 q" : "\x1b[4 q"); data->unibi_ext.set_cursor_shape_block = - (int)unibi_add_ext_str(ut, NULL, "\x1b[2 q"); + (int)unibi_add_ext_str(ut, NULL, cusr_blink ? "\x1b[1 q" : "\x1b[2 q"); } end: diff --git a/src/nvim/version.c b/src/nvim/version.c index 3d81788a7e..9cf509ca23 100644 --- a/src/nvim/version.c +++ b/src/nvim/version.c @@ -366,7 +366,7 @@ static int included_patches[] = { 2077, // 2076, 2075, - // 2074, + 2074, // 2073 NA // 2072, 2071, @@ -526,7 +526,7 @@ static int included_patches[] = { // 1917 NA // 1916 NA // 1915 NA - // 1914, + // 1914 NA 1913, 1912, // 1911 NA diff --git a/src/nvim/vim.h b/src/nvim/vim.h index 8271abda8d..458d23fcad 100644 --- a/src/nvim/vim.h +++ b/src/nvim/vim.h @@ -269,6 +269,7 @@ enum { #define STRCAT(d, s) strcat((char *)(d), (char *)(s)) #define STRNCAT(d, s, n) strncat((char *)(d), (char *)(s), (size_t)(n)) +#define STRLCAT(d, s, n) xstrlcat((char *)(d), (char *)(s), (size_t)(n)) # define vim_strpbrk(s, cs) (char_u *)strpbrk((char *)(s), (char *)(cs)) diff --git a/test/functional/core/job_spec.lua b/test/functional/core/job_spec.lua index 38c2d4e9cc..6e9633465f 100644 --- a/test/functional/core/job_spec.lua +++ b/test/functional/core/job_spec.lua @@ -42,10 +42,12 @@ describe('jobs', function() end) it('uses &shell and &shellcmdflag if passed a string', function() - -- TODO: Windows: jobstart() does not inherit $VAR - if helpers.pending_win32(pending) then return end nvim('command', "let $VAR = 'abc'") - nvim('command', "let j = jobstart('echo $VAR', g:job_opts)") + if iswin() then + nvim('command', "let j = jobstart('echo $env:VAR', g:job_opts)") + else + nvim('command', "let j = jobstart('echo $VAR', g:job_opts)") + end eq({'notification', 'stdout', {0, {'abc', ''}}}, next_msg()) eq({'notification', 'exit', {0, 0}}, next_msg()) end) diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua index d9c85c03f8..7de1d0f2c6 100644 --- a/test/functional/helpers.lua +++ b/test/functional/helpers.lua @@ -13,6 +13,8 @@ local check_logs = global_helpers.check_logs local neq = global_helpers.neq local eq = global_helpers.eq local ok = global_helpers.ok +local map = global_helpers.map +local filter = global_helpers.filter local start_dir = lfs.currentdir() local nvim_prog = os.getenv('NVIM_PROG') or 'build/bin/nvim' @@ -30,13 +32,9 @@ local uname = global_helpers.uname -- when the build is not in the default location. local nvim_dir = nvim_prog:gsub("[/\\][^/\\]+$", "") if nvim_dir == nvim_prog then - nvim_dir = "." + nvim_dir = "." end --- Nvim "Unit Under Test" http://en.wikipedia.org/wiki/Device_under_test -local NvimUUT = {} -NvimUUT.__index = NvimUUT - local prepend_argv if os.getenv('VALGRIND') then @@ -542,68 +540,72 @@ local curbufmeths = create_callindex(curbuf) local curwinmeths = create_callindex(curwin) local curtabmeths = create_callindex(curtab) +local M = { + prepend_argv = prepend_argv, + clear = clear, + connect = connect, + retry = retry, + spawn = spawn, + dedent = dedent, + source = source, + rawfeed = rawfeed, + insert = insert, + iswin = iswin, + feed = feed, + execute = execute, + eval = nvim_eval, + call = nvim_call, + command = nvim_command, + request = request, + next_message = next_message, + run = run, + stop = stop, + eq = eq, + neq = neq, + expect = expect, + ok = ok, + map = map, + filter = filter, + nvim = nvim, + nvim_async = nvim_async, + nvim_prog = nvim_prog, + nvim_dir = nvim_dir, + buffer = buffer, + window = window, + tabpage = tabpage, + curbuf = curbuf, + curwin = curwin, + curtab = curtab, + curbuf_contents = curbuf_contents, + wait = wait, + sleep = sleep, + set_session = set_session, + write_file = write_file, + os_name = os_name, + rmdir = rmdir, + mkdir = lfs.mkdir, + exc_exec = exc_exec, + redir_exec = redir_exec, + merge_args = merge_args, + funcs = funcs, + meths = meths, + bufmeths = bufmeths, + winmeths = winmeths, + tabmeths = tabmeths, + uimeths = uimeths, + curbufmeths = curbufmeths, + curwinmeths = curwinmeths, + curtabmeths = curtabmeths, + pending_win32 = pending_win32, + skip_fragile = skip_fragile, + set_shell_powershell = set_shell_powershell, + tmpname = tmpname, + NIL = mpack.NIL, +} + return function(after_each) if after_each then after_each(check_logs) end - return { - prepend_argv = prepend_argv, - clear = clear, - connect = connect, - retry = retry, - spawn = spawn, - dedent = dedent, - source = source, - rawfeed = rawfeed, - insert = insert, - iswin = iswin, - feed = feed, - execute = execute, - eval = nvim_eval, - call = nvim_call, - command = nvim_command, - request = request, - next_message = next_message, - run = run, - stop = stop, - eq = eq, - neq = neq, - expect = expect, - ok = ok, - nvim = nvim, - nvim_async = nvim_async, - nvim_prog = nvim_prog, - nvim_dir = nvim_dir, - buffer = buffer, - window = window, - tabpage = tabpage, - curbuf = curbuf, - curwin = curwin, - curtab = curtab, - curbuf_contents = curbuf_contents, - wait = wait, - sleep = sleep, - set_session = set_session, - write_file = write_file, - os_name = os_name, - rmdir = rmdir, - mkdir = lfs.mkdir, - exc_exec = exc_exec, - redir_exec = redir_exec, - merge_args = merge_args, - funcs = funcs, - meths = meths, - bufmeths = bufmeths, - winmeths = winmeths, - tabmeths = tabmeths, - uimeths = uimeths, - curbufmeths = curbufmeths, - curwinmeths = curwinmeths, - curtabmeths = curtabmeths, - pending_win32 = pending_win32, - skip_fragile = skip_fragile, - set_shell_powershell = set_shell_powershell, - tmpname = tmpname, - NIL = mpack.NIL, - } + return M end diff --git a/test/functional/normal/put_spec.lua b/test/functional/normal/put_spec.lua new file mode 100644 index 0000000000..36d5e8b43c --- /dev/null +++ b/test/functional/normal/put_spec.lua @@ -0,0 +1,936 @@ +local Screen = require('test.functional.ui.screen') +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local insert = helpers.insert +local feed = helpers.feed +local expect = helpers.expect +local eq = helpers.eq +local map = helpers.map +local filter = helpers.filter +local execute = helpers.execute +local curbuf_contents = helpers.curbuf_contents +local funcs = helpers.funcs +local dedent = helpers.dedent +local getreg = funcs.getreg + +local function reset() + clear() + insert([[ + Line of words 1 + Line of words 2]]) + execute('goto 1') + feed('itest_string.<esc>u') + funcs.setreg('a', 'test_stringa', 'V') + funcs.setreg('b', 'test_stringb\ntest_stringb\ntest_stringb', 'b') + funcs.setreg('"', 'test_string"', 'v') +end + +-- We check the last inserted register ". in each of these tests because it is +-- implemented completely differently in do_put(). +-- It is implemented differently so that control characters and imap'ped +-- characters work in the same manner when pasted as when inserted. +describe('put command', function() + -- Put a call to clear() here to force the connection to the server. + -- This means we can use the funcs.*() functions while mangling text before + -- the actual tests are run. + clear() + before_each(reset) + + local function visual_marks_zero() + for _,v in pairs(funcs.getpos("'<")) do + if v ~= 0 then + return false + end + end + for _,v in pairs(funcs.getpos("'>")) do + if v ~= 0 then + return false + end + end + return true + end + + -- {{{ Where test definitions are run + local function run_test_variations(test_variations, extra_setup) + reset() + if extra_setup then extra_setup() end + local init_contents = curbuf_contents() + local init_cursorpos = funcs.getcurpos() + local assert_no_change = function (exception_table, after_undo) + expect(init_contents) + -- When putting the ". register forwards, undo doesn't move + -- the cursor back to where it was before. + -- This is because it uses the command character 'a' to + -- start the insert, and undo after that leaves the cursor + -- one place to the right (unless we were at the end of the + -- line when we pasted). + if not (exception_table.undo_position and after_undo) then + eq(funcs.getcurpos(), init_cursorpos) + end + end + + for _, test in pairs(test_variations) do + it(test.description, function() + if extra_setup then extra_setup() end + local orig_dotstr = funcs.getreg('.') + helpers.ok(visual_marks_zero()) + -- Make sure every test starts from the same conditions + assert_no_change(test.exception_table, false) + local was_cli = test.test_action() + test.test_assertions(test.exception_table, false) + -- Check that undo twice puts us back to the original conditions + -- (i.e. puts the cursor and text back to before) + feed('u') + assert_no_change(test.exception_table, true) + + -- Should not have changed the ". register + -- If we paste the ". register with a count we can't avoid + -- changing this register, hence avoid this check. + if not test.exception_table.dot_reg_changed then + eq(funcs.getreg('.'), orig_dotstr) + end + + -- Doing something, undoing it, and then redoing it should + -- leave us in the same state as just doing it once. + -- For :ex actions we want '@:', for normal actions we want '.' + + -- The '.' redo doesn't work for visual put so just exit if + -- it was tested. + -- We check that visual put was used by checking if the '< and + -- '> marks were changed. + if not visual_marks_zero() then + return + end + + if test.exception_table.undo_position then + funcs.setpos('.', init_cursorpos) + end + if was_cli then + feed('@:') + else + feed('.') + end + + test.test_assertions(test.exception_table, true) + end) + end + end -- run_test_variations() + -- }}} + + local function create_test_defs(test_defs, command_base, command_creator, -- {{{ + expect_base, expect_creator) + local rettab = {} + local exceptions + for _, v in pairs(test_defs) do + if v[4] then + exceptions = v[4] + else + exceptions = {} + end + table.insert(rettab, + { + test_action = command_creator(command_base, v[1]), + test_assertions = expect_creator(expect_base, v[2]), + description = v[3], + exception_table = exceptions, + }) + end + return rettab + end -- create_test_defs() }}} + + local function find_cursor_position(expect_string) -- {{{ + -- There must only be one occurance of the character 'x' in + -- expect_string. + -- This function removes that occurance, and returns the position that + -- it was in. + -- This returns the cursor position that would leave the 'x' in that + -- place if we feed 'ix<esc>' and the string existed before it. + for linenum, line in pairs(funcs.split(expect_string, '\n', 1)) do + local column = line:find('x') + if column then + return {linenum, column}, expect_string:gsub('x', '') + end + end + end -- find_cursor_position() }}} + + -- Action function creators {{{ + local function create_p_action(test_map, substitution) + local temp_val = test_map:gsub('p', substitution) + return function() + feed(temp_val) + return false + end + end + + local function create_put_action(command_base, substitution) + local temp_val = command_base:gsub('put', substitution) + return function() + execute(temp_val) + return true + end + end + -- }}} + + -- Expect function creator {{{ + local function expect_creator(conversion_function, expect_base, conversion_table) + local temp_expect_string = conversion_function(expect_base, conversion_table) + local cursor_position, expect_string = find_cursor_position(temp_expect_string) + return function(exception_table, after_redo) + expect(expect_string) + + -- Have to use getcurpos() instead of curwinmeths.get_cursor() in + -- order to account for virtualedit. + -- We always want the curswant element in getcurpos(), which is + -- sometimes different to the column element in + -- curwinmeths.get_cursor(). + -- NOTE: The ".gp command leaves the cursor after the pasted text + -- when running, but does not when the command is redone with the + -- '.' command. + if not (exception_table.redo_position and after_redo) then + local actual_position = funcs.getcurpos() + eq(cursor_position, {actual_position[2], actual_position[5]}) + end + end + end -- expect_creator() }}} + + -- Test definitions {{{ + local function copy_def(def) + local rettab = { '', {}, '', nil } + rettab[1] = def[1] + for k,v in pairs(def[2]) do + rettab[2][k] = v + end + rettab[3] = def[3] + if def[4] then + rettab[4] = {} + for k,v in pairs(def[4]) do + rettab[4][k] = v + end + end + return rettab + end + + local normal_command_defs = { + { + 'p', + {cursor_after = false, put_backwards = false, dot_register = false}, + 'pastes after cursor with p', + }, + { + 'gp', + {cursor_after = true, put_backwards = false, dot_register = false}, + 'leaves cursor after text with gp', + }, + { + '".p', + {cursor_after = false, put_backwards = false, dot_register = true}, + 'works with the ". register', + }, + { + '".gp', + {cursor_after = true, put_backwards = false, dot_register = true}, + 'gp works with the ". register', + {redo_position = true}, + }, + { + 'P', + {cursor_after = false, put_backwards = true, dot_register = false}, + 'pastes before cursor with P', + }, + { + 'gP', + {cursor_after = true, put_backwards = true, dot_register = false}, + 'gP pastes before cursor and leaves cursor after text', + }, + { + '".P', + {cursor_after = false, put_backwards = true, dot_register = true}, + 'P works with ". register', + }, + { + '".gP', + {cursor_after = true, put_backwards = true, dot_register = true}, + 'gP works with ". register', + {redo_position = true}, + }, + } + + -- Add a definition applying a count for each definition above. + -- Could do this for each transformation (p -> P, p -> gp etc), but I think + -- it's neater this way (balance between being explicit and too verbose). + for i = 1,#normal_command_defs do + local cur = normal_command_defs[i] + + -- Make modified copy of current definition that includes a count. + local newdef = copy_def(cur) + newdef[2].count = 2 + cur[2].count = 1 + newdef[1] = '2' .. newdef[1] + newdef[3] = 'double ' .. newdef[3] + + if cur[2].dot_register then + if not cur[4] then + newdef[4] = {} + end + newdef[4].dot_reg_changed = true + end + + normal_command_defs[#normal_command_defs + 1] = newdef + end + + local ex_command_defs = { + { + 'put', + {put_backwards = false, dot_register = false}, + 'pastes linewise forwards with :put', + }, + { + 'put!', + {put_backwards = true, dot_register = false}, + 'pastes linewise backwards with :put!', + }, + { + 'put .', + {put_backwards = false, dot_register = true}, + 'pastes linewise with the dot register', + }, + { + 'put! .', + {put_backwards = true, dot_register = true}, + 'pastes linewise backwards with the dot register', + }, + } + + local function non_dotdefs(def_table) + return filter(function(d) return not d[2].dot_register end, def_table) + end + + -- }}} + + -- Conversion functions {{{ + local function convert_characterwise(expect_base, conversion_table, + virtualedit_end, visual_put) + expect_base = dedent(expect_base) + -- There is no difference between 'P' and 'p' when VIsual_active + if not visual_put then + if conversion_table.put_backwards then + -- Special case for virtualedit at the end of a line. + local replace_string + if not virtualedit_end then + replace_string = 'test_stringx"%1' + else + replace_string = 'test_stringx"' + end + expect_base = expect_base:gsub('(.)test_stringx"', replace_string) + end + end + if conversion_table.count > 1 then + local rep_string = 'test_string"' + local extra_puts = rep_string:rep(conversion_table.count - 1) + expect_base = expect_base:gsub('test_stringx"', extra_puts .. 'test_stringx"') + end + if conversion_table.cursor_after then + expect_base = expect_base:gsub('test_stringx"', 'test_string"x') + end + if conversion_table.dot_register then + expect_base = expect_base:gsub('(test_stringx?)"', '%1.') + end + return expect_base + end -- convert_characterwise() + + local function make_back(string) + local prev_line + local rettab = {} + local string_found = false + for _, line in pairs(funcs.split(string, '\n', 1)) do + if line:find('test_string') then + string_found = true + table.insert(rettab, line) + else + if string_found then + if prev_line then + table.insert(rettab, prev_line) + prev_line = nil + end + table.insert(rettab, line) + else + table.insert(rettab, prev_line) + prev_line = line + end + end + end + -- In case there are no lines after the text that was put. + if prev_line and string_found then + table.insert(rettab, prev_line) + end + return table.concat(rettab, '\n') + end -- make_back() + + local function convert_linewise(expect_base, conversion_table, _, use_a, indent) + expect_base = dedent(expect_base) + if conversion_table.put_backwards then + expect_base = make_back(expect_base) + end + local p_str = 'test_string"' + if use_a then + p_str = 'test_stringa' + end + + if conversion_table.dot_register then + expect_base = expect_base:gsub('x' .. p_str, 'xtest_string.') + p_str = 'test_string.' + end + + if conversion_table.cursor_after then + expect_base = expect_base:gsub('x' .. p_str .. '\n', p_str .. '\nx') + end + + -- The 'indent' argument is only used here because a single put with an + -- indent doesn't require special handling. It doesn't require special + -- handling because the cursor is never put before the indent, hence + -- the modification of 'test_stringx"' gives the same overall answer as + -- modifying ' test_stringx"'. + + -- Only happens when using normal mode command actions. + if conversion_table.count and conversion_table.count > 1 then + if not indent then + indent = '' + end + local rep_string = indent .. p_str .. '\n' + local extra_puts = rep_string:rep(conversion_table.count - 1) + local orig_string, new_string + if conversion_table.cursor_after then + orig_string = indent .. p_str .. '\nx' + new_string = extra_puts .. orig_string + else + orig_string = indent .. 'x' .. p_str .. '\n' + new_string = orig_string .. extra_puts + end + expect_base = expect_base:gsub(orig_string, new_string) + end + return expect_base + end + + local function put_x_last(orig_line, p_str) + local prev_end, cur_end, cur_start = 0, 0, 0 + while cur_start do + prev_end = cur_end + cur_start, cur_end = orig_line:find(p_str, prev_end) + end + -- Assume (because that is the only way I call it) that p_str matches + -- the pattern 'test_string.' + return orig_line:sub(1, prev_end - 1) .. 'x' .. orig_line:sub(prev_end) + end + + local function convert_blockwise(expect_base, conversion_table, visual, + use_b, trailing_whitespace) + expect_base = dedent(expect_base) + local p_str = 'test_string"' + if use_b then + p_str = 'test_stringb' + end + + if conversion_table.dot_register then + expect_base = expect_base:gsub('(x?)' .. p_str, '%1test_string.') + -- Looks strange, but the dot is a special character in the pattern + -- and a literal character in the replacement. + expect_base = expect_base:gsub('test_stringx.', 'test_stringx.') + p_str = 'test_string.' + end + + -- No difference between 'p' and 'P' in visual mode. + if not visual then + if conversion_table.put_backwards then + -- One for the line where the cursor is left, one for all other + -- lines. + expect_base = expect_base:gsub('([^x])' .. p_str, p_str .. '%1') + expect_base = expect_base:gsub('([^x])x' .. p_str, 'x' .. p_str .. '%1') + if not trailing_whitespace then + expect_base = expect_base:gsub(' \n', '\n') + expect_base = expect_base:gsub(' $', '') + end + end + end + + if conversion_table.count and conversion_table.count > 1 then + local p_pattern = p_str:gsub('%.', '%%.') + expect_base = expect_base:gsub(p_pattern, + p_str:rep(conversion_table.count)) + expect_base = expect_base:gsub('test_stringx([b".])', + p_str:rep(conversion_table.count - 1) + .. '%0') + end + + if conversion_table.cursor_after then + if not visual then + local prev_line + local rettab = {} + local prev_in_block = false + for _, line in pairs(funcs.split(expect_base, '\n', 1)) do + if line:find('test_string') then + if prev_line then + prev_line = prev_line:gsub('x', '') + table.insert(rettab, prev_line) + end + prev_line = line + prev_in_block = true + else + if prev_in_block then + prev_line = put_x_last(prev_line, p_str) + table.insert(rettab, prev_line) + prev_in_block = false + end + table.insert(rettab, line) + end + end + if prev_line and prev_in_block then + table.insert(rettab, put_x_last(prev_line, p_str)) + end + + expect_base = table.concat(rettab, '\n') + else + expect_base = expect_base:gsub('x(.)', '%1x') + end + end + + return expect_base + end + -- }}} + + -- Convenience functions {{{ + local function run_normal_mode_tests(test_string, base_map, extra_setup, + virtualedit_end, selection_string) + local function convert_closure(e, c) + return convert_characterwise(e, c, virtualedit_end, selection_string) + end + local function expect_normal_creator(expect_base, conversion_table) + local test_expect = expect_creator(convert_closure, expect_base, conversion_table) + return function(exception_table, after_redo) + test_expect(exception_table, after_redo) + if selection_string then + eq(getreg('"'), selection_string) + else + eq(getreg('"'), 'test_string"') + end + end + end + run_test_variations( + create_test_defs( + normal_command_defs, + base_map, + create_p_action, + test_string, + expect_normal_creator + ), + extra_setup + ) + end -- run_normal_mode_tests() + + local function convert_linewiseer(expect_base, conversion_table) + return expect_creator(convert_linewise, expect_base, conversion_table) + end + + local function run_linewise_tests(expect_base, base_command, extra_setup) + local linewise_test_defs = create_test_defs( + ex_command_defs, base_command, + create_put_action, expect_base, convert_linewiseer) + run_test_variations(linewise_test_defs, extra_setup) + end -- run_linewise_tests() + -- }}} + + -- Actual tests + describe('default pasting', function() + local expect_string = [[ + Ltest_stringx"ine of words 1 + Line of words 2]] + run_normal_mode_tests(expect_string, 'p') + + run_linewise_tests([[ + Line of words 1 + xtest_string" + Line of words 2]], + 'put' + ) + end) + + describe('linewise register', function() + -- put with 'p' + local local_ex_command_defs = non_dotdefs(normal_command_defs) + local base_expect_string = [[ + Line of words 1 + xtest_stringa + Line of words 2]] + local function local_convert_linewise(expect_base, conversion_table) + return convert_linewise(expect_base, conversion_table, nil, true) + end + local function expect_lineput(expect_base, conversion_table) + return expect_creator(local_convert_linewise, expect_base, conversion_table) + end + run_test_variations( + create_test_defs( + local_ex_command_defs, + '"ap', + create_p_action, + base_expect_string, + expect_lineput + ) + ) + + -- put with :put + local linewise_put_defs = non_dotdefs(ex_command_defs) + base_expect_string = [[ + Line of words 1 + xtest_stringa + Line of words 2]] + run_test_variations( + create_test_defs( + linewise_put_defs, + 'put a', create_put_action, + base_expect_string, convert_linewiseer + ) + ) + + end) + + describe('blockwise register', function() + local blockwise_put_defs = non_dotdefs(normal_command_defs) + local test_base = [[ + Lxtest_stringbine of words 1 + Ltest_stringbine of words 2 + test_stringb]] + + local function expect_block_creator(expect_base, conversion_table) + return expect_creator(function(e,c) return convert_blockwise(e,c,nil,true) end, + expect_base, conversion_table) + end + + run_test_variations( + create_test_defs( + blockwise_put_defs, + '"bp', + create_p_action, + test_base, + expect_block_creator + ) + ) + end) + + it('adds correct indentation when put with [p and ]p', function() + feed('G>>"a]pix<esc>') + -- luacheck: ignore + expect([[ + Line of words 1 + Line of words 2 + xtest_stringa]]) + feed('uu"a[pix<esc>') + -- luacheck: ignore + expect([[ + Line of words 1 + xtest_stringa + Line of words 2]]) + end) + + describe('linewise paste with autoindent', function() + -- luacheck: ignore + run_linewise_tests([[ + Line of words 1 + Line of words 2 + xtest_string"]], + 'put' + , + function() + funcs.setline('$', ' Line of words 2') + -- Set curswant to '8' to be at the end of the tab character + -- This is where the cursor is put back after the 'u' command. + funcs.setpos('.', {0, 2, 1, 0, 8}) + execute('set autoindent') + end + ) + end) + + describe('put inside tabs with virtualedit', function() + local test_string = [[ + Line of words 1 + test_stringx" Line of words 2]] + run_normal_mode_tests(test_string, 'p', function() + funcs.setline('$', ' Line of words 2') + execute('set virtualedit=all') + funcs.setpos('.', {0, 2, 1, 2, 3}) + end) + end) + + describe('put after the line with virtualedit', function() + local test_string = [[ + Line of words 1 test_stringx" + Line of words 2]] + run_normal_mode_tests(test_string, 'p', function() + funcs.setline('$', ' Line of words 2') + execute('set virtualedit=all') + funcs.setpos('.', {0, 1, 16, 1, 17}) + end, true) + end) + + describe('Visual put', function() + describe('basic put', function() + local test_string = [[ + test_stringx" words 1 + Line of words 2]] + run_normal_mode_tests(test_string, 'v2ep', nil, nil, 'Line of') + end) + describe('over trailing newline', function() + local test_string = 'Line of test_stringx"Line of words 2' + run_normal_mode_tests(test_string, 'v$p', function() + funcs.setpos('.', {0, 1, 9, 0, 9}) + end, + nil, + 'words 1\n') + end) + describe('linewise mode', function() + local test_string = [[ + xtest_string" + Line of words 2]] + local function expect_vis_linewise(expect_base, conversion_table) + return expect_creator(function(e, c) + return convert_linewise(e, c, nil, nil) + end, + expect_base, conversion_table) + end + run_test_variations( + create_test_defs( + normal_command_defs, + 'Vp', + create_p_action, + test_string, + expect_vis_linewise + ), + function() funcs.setpos('.', {0, 1, 1, 0, 1}) end + ) + + describe('with whitespace at bol', function() + local function expect_vis_lineindented(expect_base, conversion_table) + local test_expect = expect_creator(function(e, c) + return convert_linewise(e, c, nil, nil, ' ') + end, + expect_base, conversion_table) + return function(exception_table, after_redo) + test_expect(exception_table, after_redo) + eq(getreg('"'), 'Line of words 1\n') + end + end + local base_expect_string = [[ + xtest_string" + Line of words 2]] + run_test_variations( + create_test_defs( + normal_command_defs, + 'Vp', + create_p_action, + base_expect_string, + expect_vis_lineindented + ), + function() + feed('i test_string.<esc>u') + funcs.setreg('"', ' test_string"', 'v') + end + ) + end) + + end) + + describe('blockwise visual mode', function() + local test_base = [[ + test_stringx"e of words 1 + test_string"e of words 2]] + + local function expect_block_creator(expect_base, conversion_table) + local test_expect = expect_creator(function(e, c) + return convert_blockwise(e, c, true) + end, expect_base, conversion_table) + return function(e,c) + test_expect(e,c) + eq(getreg('"'), 'Lin\nLin') + end + end + + local select_down_test_defs = create_test_defs( + normal_command_defs, + '<C-v>jllp', + create_p_action, + test_base, + expect_block_creator + ) + run_test_variations(select_down_test_defs) + + + -- Undo and redo of a visual block put leave the cursor in the top + -- left of the visual block area no matter where the cursor was + -- when it started. + local undo_redo_no = map(function(table) + local rettab = copy_def(table) + if not rettab[4] then + rettab[4] = {} + end + rettab[4].undo_position = true + rettab[4].redo_position = true + return rettab + end, + normal_command_defs) + + -- Selection direction doesn't matter + run_test_variations( + create_test_defs( + undo_redo_no, + '<C-v>kllp', + create_p_action, + test_base, + expect_block_creator + ), + function() funcs.setpos('.', {0, 2, 1, 0, 1}) end + ) + + describe('blockwise cursor after undo', function() + -- A bit of a hack of the reset above. + -- In the tests that selection direction doesn't matter, we + -- don't check the undo/redo position because it doesn't fit + -- the same pattern as everything else. + -- Here we fix this by directly checking the undo/redo position + -- in the test_assertions of our test definitions. + local function assertion_creator(_,_) + return function(_,_) + feed('u') + -- Have to use feed('u') here to set curswant, because + -- ex_undo() doesn't do that. + eq(funcs.getcurpos(), {0, 1, 1, 0, 1}) + feed('<C-r>') + eq(funcs.getcurpos(), {0, 1, 1, 0, 1}) + end + end + + run_test_variations( + create_test_defs( + undo_redo_no, + '<C-v>kllp', + create_p_action, + test_base, + assertion_creator + ), + function() funcs.setpos('.', {0, 2, 1, 0, 1}) end + ) + end) + end) + + + describe("with 'virtualedit'", function() + describe('splitting a tab character', function() + local base_expect_string = [[ + Line of words 1 + test_stringx" Line of words 2]] + run_normal_mode_tests( + base_expect_string, + 'vp', + function() + funcs.setline('$', ' Line of words 2') + execute('set virtualedit=all') + funcs.setpos('.', {0, 2, 1, 2, 3}) + end, + nil, + ' ' + ) + end) + describe('after end of line', function() + local base_expect_string = [[ + Line of words 1 test_stringx" + Line of words 2]] + run_normal_mode_tests( + base_expect_string, + 'vp', + function() + execute('set virtualedit=all') + funcs.setpos('.', {0, 1, 16, 2, 18}) + end, + true, + ' ' + ) + end) + end) + end) + + describe('. register special tests', function() + before_each(reset) + it('applies control character actions', function() + feed('i<C-t><esc>u') + expect([[ + Line of words 1 + Line of words 2]]) + feed('".p') + expect([[ + Line of words 1 + Line of words 2]]) + feed('u1go<C-v>j".p') + eq([[ + ine of words 1 + ine of words 2]], curbuf_contents()) + end) + + local function bell_test(actions, should_ring) + local screen = Screen.new() + screen:attach() + helpers.ok(not screen.bell and not screen.visualbell) + actions() + helpers.wait() + screen:wait(function() + if should_ring then + if not screen.bell and not screen.visualbell then + return 'Bell was not rung after action' + end + else + if screen.bell or screen.visualbell then + return 'Bell was rung after action' + end + end + end) + screen:detach() + end + + it('should not ring the bell with gp at end of line', function() + bell_test(function() feed('$".gp') end) + + -- Even if the last character is a multibyte character. + reset() + funcs.setline(1, 'helloม') + bell_test(function() feed('$".gp') end) + end) + + it('should not ring the bell with gp and end of file', function() + funcs.setpos('.', {0, 2, 1, 0}) + bell_test(function() feed('$vl".gp') end) + end) + + it('should ring the bell when deleting if not appropriate', function() + execute('goto 2') + feed('i<bs><esc>') + expect([[ + ine of words 1 + Line of words 2]]) + bell_test(function() feed('".P') end, true) + end) + + it('should restore cursor position after undo of ".p', function() + local origpos = funcs.getcurpos() + feed('".pu') + eq(origpos, funcs.getcurpos()) + end) + + it("should be unaffected by 'autoindent' with V\".2p", function() + execute('set autoindent') + feed('i test_string.<esc>u') + feed('V".2p') + expect([[ + test_string. + test_string. + Line of words 2]]) + end) + end) +end) + diff --git a/test/functional/ui/output_spec.lua b/test/functional/ui/output_spec.lua index 47b2516188..d0166bc1c1 100644 --- a/test/functional/ui/output_spec.lua +++ b/test/functional/ui/output_spec.lua @@ -41,6 +41,11 @@ describe("shell command :!", function() end) it("throttles shell-command output greater than ~10KB", function() + if os.getenv("TRAVIS") and helpers.os_name() == "osx" then + pending("[Unreliable on Travis macOS.]", function() end) + return + end + screen.timeout = 20000 -- Avoid false failure on slow systems. child_session.feed_data( ":!for i in $(seq 2 3000); do echo XXXXXXXXXX $i; done\n") diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua index ef1f0783e7..54f43387dc 100644 --- a/test/functional/ui/screen.lua +++ b/test/functional/ui/screen.lua @@ -68,7 +68,8 @@ -- }) -- screen:set_default_attr_ignore( {{}, {bold=true, foreground=NonText}} ) -- --- To help write screen tests, see screen:snapshot_util(). +-- To help write screen tests, see Screen:snapshot_util(). +-- To debug screen tests, see Screen:redraw_debug(). local helpers = require('test.functional.helpers')(nil) local request, run, uimeths = helpers.request, helpers.run, helpers.uimeths @@ -520,9 +521,11 @@ function Screen:_current_screen() return table.concat(rv, '\n') end --- Utility to generate/debug tests. Call it where screen:expect() would be. --- Waits briefly, then dumps the current screen state in the form of --- screen:expect(). Use snapshot_util({},true) to generate a text-only test. +-- Generates tests. Call it where Screen:expect() would be. Waits briefly, then +-- dumps the current screen state in the form of Screen:expect(). +-- Use snapshot_util({},true) to generate a text-only (no attributes) test. +-- +-- @see Screen:redraw_debug() function Screen:snapshot_util(attrs, ignore) self:sleep(250) self:print_snapshot(attrs, ignore) @@ -610,7 +613,7 @@ function Screen:_pprint_attrs(attrs) return table.concat(items, ", ") end -function backward_find_meaningful(tbl, from) -- luacheck: ignore +local function backward_find_meaningful(tbl, from) -- luacheck: no unused for i = from or #tbl, 1, -1 do if tbl[i] ~= ' ' then return i + 1 diff --git a/test/helpers.lua b/test/helpers.lua index def0740f85..3f7a9c2b74 100644 --- a/test/helpers.lua +++ b/test/helpers.lua @@ -91,6 +91,24 @@ local function tmpname() end end +local function map(func, tab) + local rettab = {} + for k, v in pairs(tab) do + rettab[k] = func(v) + end + return rettab +end + +local function filter(filter_func, tab) + local rettab = {} + for _, entry in pairs(tab) do + if filter_func(entry) then + table.insert(rettab, entry) + end + end + return rettab +end + return { eq = eq, neq = neq, @@ -98,4 +116,6 @@ return { check_logs = check_logs, uname = uname, tmpname = tmpname, + map = map, + filter = filter, } diff --git a/test/unit/os/env_spec.lua b/test/unit/os/env_spec.lua index 9e00a3e8f8..64bbaaa8c2 100644 --- a/test/unit/os/env_spec.lua +++ b/test/unit/os/env_spec.lua @@ -10,19 +10,19 @@ local NULL = helpers.NULL require('lfs') -local env = cimport('./src/nvim/os/os.h') +local cimp = cimport('./src/nvim/os/os.h') describe('env function', function() local function os_setenv(name, value, override) - return env.os_setenv((to_cstr(name)), (to_cstr(value)), override) + return cimp.os_setenv((to_cstr(name)), (to_cstr(value)), override) end local function os_unsetenv(name, _, _) - return env.os_unsetenv((to_cstr(name))) + return cimp.os_unsetenv((to_cstr(name))) end local function os_getenv(name) - local rval = env.os_getenv((to_cstr(name))) + local rval = cimp.os_getenv((to_cstr(name))) if rval ~= NULL then return ffi.string(rval) else @@ -88,14 +88,14 @@ describe('env function', function() local i = 0 local names = { } local found_name = false - local name = env.os_getenvname_at_index(i) + local name = cimp.os_getenvname_at_index(i) while name ~= NULL do table.insert(names, ffi.string(name)) if (ffi.string(name)) == test_name then found_name = true end i = i + 1 - name = env.os_getenvname_at_index(i) + name = cimp.os_getenvname_at_index(i) end eq(true, (table.getn(names)) > 0) eq(true, found_name) @@ -104,15 +104,15 @@ describe('env function', function() it('returns NULL if the index is out of bounds', function() local huge = ffi.new('size_t', 10000) local maxuint32 = ffi.new('size_t', 4294967295) - eq(NULL, env.os_getenvname_at_index(huge)) - eq(NULL, env.os_getenvname_at_index(maxuint32)) + eq(NULL, cimp.os_getenvname_at_index(huge)) + eq(NULL, cimp.os_getenvname_at_index(maxuint32)) if ffi.abi('64bit') then -- couldn't use a bigger number because it gets converted to -- double somewere, should be big enough anyway -- maxuint64 = ffi.new 'size_t', 18446744073709551615 local maxuint64 = ffi.new('size_t', 18446744073709000000) - eq(NULL, env.os_getenvname_at_index(maxuint64)) + eq(NULL, cimp.os_getenvname_at_index(maxuint64)) end end) end) @@ -124,10 +124,10 @@ describe('env function', function() local stat_str = stat_file:read('*l') stat_file:close() local pid = tonumber((stat_str:match('%d+'))) - eq(pid, tonumber(env.os_get_pid())) + eq(pid, tonumber(cimp.os_get_pid())) else -- /proc is not available on all systems, test if pid is nonzero. - eq(true, (env.os_get_pid() > 0)) + eq(true, (cimp.os_get_pid() > 0)) end end) end) @@ -138,7 +138,7 @@ describe('env function', function() local hostname = handle:read('*l') handle:close() local hostname_buf = cstr(255, '') - env.os_get_hostname(hostname_buf, 255) + cimp.os_get_hostname(hostname_buf, 255) eq(hostname, (ffi.string(hostname_buf))) end) end) @@ -155,39 +155,52 @@ describe('env function', function() local output_buff1 = cstr(255, '') local output_buff2 = cstr(255, '') local output_expected = 'NEOVIM_UNIT_TEST_EXPAND_ENV_ESCV/test' - env.expand_env_esc(input1, output_buff1, 255, false, true, NULL) - env.expand_env_esc(input2, output_buff2, 255, false, true, NULL) + cimp.expand_env_esc(input1, output_buff1, 255, false, true, NULL) + cimp.expand_env_esc(input2, output_buff2, 255, false, true, NULL) eq(output_expected, ffi.string(output_buff1)) eq(output_expected, ffi.string(output_buff2)) end) - it('expands ~ once when one is true', function() + it('expands ~ once when `one` is true', function() local input = '~/foo ~ foo' local homedir = cstr(255, '') - env.expand_env_esc(to_cstr('~'), homedir, 255, false, true, NULL) + cimp.expand_env_esc(to_cstr('~'), homedir, 255, false, true, NULL) local output_expected = ffi.string(homedir) .. "/foo ~ foo" local output = cstr(255, '') - env.expand_env_esc(to_cstr(input), output, 255, false, true, NULL) + cimp.expand_env_esc(to_cstr(input), output, 255, false, true, NULL) eq(ffi.string(output), ffi.string(output_expected)) end) - it('expands ~ every time when one is false', function() + it('expands ~ every time when `one` is false', function() local input = to_cstr('~/foo ~ foo') - local homedir = cstr(255, '') - env.expand_env_esc(to_cstr('~'), homedir, 255, false, true, NULL) - homedir = ffi.string(homedir) + local dst = cstr(255, '') + cimp.expand_env_esc(to_cstr('~'), dst, 255, false, true, NULL) + local homedir = ffi.string(dst) local output_expected = homedir .. "/foo " .. homedir .. " foo" local output = cstr(255, '') - env.expand_env_esc(input, output, 255, false, false, NULL) + cimp.expand_env_esc(input, output, 255, false, false, NULL) eq(output_expected, ffi.string(output)) end) - it('respects the dstlen parameter without expansion', function() + it('does not crash #3725', function() + local name_out = ffi.new('char[100]') + cimp.os_get_user_name(name_out, 100) + local curuser = ffi.string(name_out) + + local src = to_cstr("~"..curuser.."/Vcs/django-rest-framework/rest_framework/renderers.py") + local dst = cstr(256, "~"..curuser) + cimp.expand_env_esc(src, dst, 1024, false, false, NULL) + local len = string.len(ffi.string(dst)) + assert.True(len > 56) + assert.True(len < 99) + end) + + it('respects `dstlen` without expansion', function() local input = to_cstr('this is a very long thing that will not fit') -- The buffer is long enough to actually contain the full input in case the -- test fails, but we don't tell expand_env_esc that local output = cstr(255, '') - env.expand_env_esc(input, output, 5, false, true, NULL) + cimp.expand_env_esc(input, output, 5, false, true, NULL) -- Make sure the first few characters are copied properly and that there is a -- terminating null character for i=0,3 do @@ -196,17 +209,17 @@ describe('env function', function() eq(0, output[4]) end) - it('respects the dstlen parameter with expansion', function() + it('respects `dstlen` with expansion', function() local varname = to_cstr('NVIM_UNIT_TEST_EXPAND_ENV_ESC_DSTLENN') local varval = to_cstr('NVIM_UNIT_TEST_EXPAND_ENV_ESC_DSTLENV') - env.os_setenv(varname, varval, 1) + cimp.os_setenv(varname, varval, 1) -- TODO(bobtwinkles) This test uses unix-specific environment variable accessing, -- should have some alternative for windows local input = to_cstr('$NVIM_UNIT_TEST_EXPAND_ENV_ESC_DSTLENN/even more stuff') -- The buffer is long enough to actually contain the full input in case the -- test fails, but we don't tell expand_env_esc that local output = cstr(255, '') - env.expand_env_esc(input, output, 5, false, true, NULL) + cimp.expand_env_esc(input, output, 5, false, true, NULL) -- Make sure the first few characters are copied properly and that there is a -- terminating null character -- expand_env_esc SHOULD NOT expand the variable if there is not enough space to |