diff options
36 files changed, 864 insertions, 285 deletions
diff --git a/.ci/clang-asan.sh b/.ci/clang-asan.sh index 4b5ff26d0d..0dff61f297 100644 --- a/.ci/clang-asan.sh +++ b/.ci/clang-asan.sh @@ -1,9 +1,9 @@ . "$CI_SCRIPTS/common.sh" -install_vroom - set_environment /opt/neovim-deps/64 +install_functional_test_deps + sudo pip install cpp-coveralls clang_version=3.4 @@ -26,14 +26,19 @@ export UBSAN_OPTIONS="log_path=$tmpdir/ubsan" # not sure if this works install_dir="$(pwd)/dist" $MAKE_CMD cmake CMAKE_EXTRA_FLAGS="-DTRAVIS_CI_BUILD=ON -DCMAKE_INSTALL_PREFIX=$install_dir -DUSE_GCOV=ON" -$MAKE_CMD if ! $MAKE_CMD test; then - reset asan_check "$tmpdir" exit 1 fi +asan_check "$tmpdir" +if ! $MAKE_CMD oldtest; then + reset + asan_check "$tmpdir" + exit 1 +fi asan_check "$tmpdir" + coveralls --encoding iso-8859-1 || echo 'coveralls upload failed.' $MAKE_CMD install diff --git a/.ci/common.sh b/.ci/common.sh index ba00011222..7939f2f4b7 100644 --- a/.ci/common.sh +++ b/.ci/common.sh @@ -1,50 +1,32 @@ valgrind_check() { - ( - cd $1 - set -- valgrind-[*] valgrind-* - case $1$2 in - 'valgrind-[*]valgrind-*') - ;; - *) - shift - local err='' - for valgrind_log in "$@"; do - # Remove useless warning - sed -i "$valgrind_log" \ - -e '/Warning: noted but unhandled ioctl/d' \ - -e '/could cause spurious value errors to appear/d' \ - -e '/See README_MISSING_SYSCALL_OR_IOCTL for guidance/d' - if [ "$(stat -c %s $valgrind_log)" != "0" ]; then - # if after removing the warning, the log still has errors, show its - # contents and set the flag so we exit with non-zero status - cat "$valgrind_log" - err=1 - fi - done - if [ -n "$err" ]; then - echo "Runtime errors detected" - exit 1 - fi - ;; - esac - ) + check_logs "$1" "valgrind-*" } asan_check() { - ( - cd $1 - set -- [*]san.[*] *san.* - case $1$2 in - '[*]san.[*]*san.*') - ;; - *) - shift - cat "$@" - echo "Runtime errors detected" - exit 1 - ;; - esac - ) + check_logs "$1" "*san.*" +} + +check_logs() { + # For some strange reason, now we need to give ubuntu some time to flush it's + # FS cache in order to see error logs, even though all commands are executing + # synchronously + sleep 1 + # Iterate through each log to remove an useless warning + for log in $(find "$1" -type f -name "$2"); do + sed -i "$log" \ + -e '/Warning: noted but unhandled ioctl/d' \ + -e '/could cause spurious value errors to appear/d' \ + -e '/See README_MISSING_SYSCALL_OR_IOCTL for guidance/d' + done + # Now do it again, but only consider files with size > 0 + for log in $(find "$1" -type f -name "$2" -size +0); do + cat "$log" + err=1 + done + if [ -n "$err" ]; then + echo "Runtime errors detected" + exit 1 + fi } set_environment() { @@ -65,14 +47,10 @@ install_prebuilt_deps() { fi } -install_vroom() { - ( +install_functional_test_deps() { sudo pip install git+https://github.com/neovim/python-client.git - git clone git://github.com/google/vroom - cd vroom - python setup.py build - sudo python setup.py install - ) + # Pass -E to let pip use PKG_CONFIG_PATH for luajit + sudo -E pip install lupa } tmpdir="$(pwd)/tmp" diff --git a/.ci/gcc-ia32.sh b/.ci/gcc-32.sh index 97e4190ccc..ec51cfab69 100644 --- a/.ci/gcc-ia32.sh +++ b/.ci/gcc-32.sh @@ -1,6 +1,9 @@ . "$CI_SCRIPTS/common.sh" -install_vroom +# To install lupa, a temporarary functional test dependency, we require the +# 64-bit luajit since travis version of python is 64-bit. +export PKG_CONFIG_PATH="/opt/neovim-deps/64/usr/lib/pkgconfig" +install_functional_test_deps set_environment /opt/neovim-deps/32 @@ -22,5 +25,7 @@ CMAKE_EXTRA_FLAGS="-DTRAVIS_CI_BUILD=ON \ -DFIND_LIBRARY_USE_LIB64_PATHS=OFF \ -DCMAKE_IGNORE_PATH=/lib:/usr/lib:/usr/local/lib \ -DCMAKE_TOOLCHAIN_FILE=cmake/i386-linux-gnu.toolchain.cmake" + $MAKE_CMD CMAKE_EXTRA_FLAGS="${CMAKE_EXTRA_FLAGS}" unittest $MAKE_CMD test +$MAKE_CMD oldtest diff --git a/.ci/gcc-unittest.sh b/.ci/gcc-unittest.sh deleted file mode 100644 index 95925ddbc9..0000000000 --- a/.ci/gcc-unittest.sh +++ /dev/null @@ -1,11 +0,0 @@ -. "$CI_SCRIPTS/common.sh" - -set_environment /opt/neovim-deps/64 - -sudo pip install cpp-coveralls - -export CC=gcc -export SKIP_EXEC=1 -$MAKE_CMD CMAKE_EXTRA_FLAGS="-DTRAVIS_CI_BUILD=ON -DUSE_GCOV=ON" unittest - -coveralls --encoding iso-8859-1 || echo 'coveralls upload failed.' diff --git a/.ci/gcc.sh b/.ci/gcc.sh new file mode 100644 index 0000000000..842e34405a --- /dev/null +++ b/.ci/gcc.sh @@ -0,0 +1,20 @@ +. "$CI_SCRIPTS/common.sh" + +set_environment /opt/neovim-deps/64 + +install_functional_test_deps + +sudo pip install cpp-coveralls + +sudo apt-get install valgrind + +export VALGRIND=1 +export VALGRIND_LOG="$tmpdir/valgrind-%p.log" +CMAKE_EXTRA_FLAGS="-DTRAVIS_CI_BUILD=ON -DUSE_GCOV=ON" + +$MAKE_CMD CMAKE_EXTRA_FLAGS="${CMAKE_EXTRA_FLAGS}" unittest +$MAKE_CMD test +valgrind_check "$tmpdir" +$MAKE_CMD oldtest + +coveralls --encoding iso-8859-1 || echo 'coveralls upload failed.' diff --git a/.travis.yml b/.travis.yml index 1965fb72b8..8c3b986aa4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,8 @@ env: - secure: "QEz92NyItkzQu52kCFD928jEwUYnA2OIgSyeNrp+Y3gm5rOmSZerY8hGiXyNZxocap9+qIPCapRRYU3ZYKWZPeucWMLN3aIjxAFdhugKbnmNYE1jFugb6b8N3SxiX/3206NHXlYaz0OZhh6OBAFmPUXamJC8OrWVgPNPo7wv4UQ=" matrix: - CI_TARGET=clang-asan - - CI_TARGET=gcc-ia32 - - CI_TARGET=gcc-unittest + - CI_TARGET=gcc + - CI_TARGET=gcc-32 - CI_TARGET=clint - CI_TARGET=api-python - CI_TARGET=coverity diff --git a/CMakeLists.txt b/CMakeLists.txt index 3622dabb23..435a215e51 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -211,6 +211,26 @@ if(BUSTED_PRG) -DBUSTED_OUTPUT_TYPE=${BUSTED_OUTPUT_TYPE} -DTEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/test -DBUILD_DIR=${CMAKE_BINARY_DIR} - -P ${CMAKE_MODULE_PATH}/RunUnittests.cmake + -DTEST_TYPE=unit + -P ${CMAKE_MODULE_PATH}/RunTests.cmake DEPENDS nvim-test unittest-headers) + + # For the functional tests we need the full path to the real busted script, + # which will be included by run-functional-tests.py. + get_filename_component(LUA_PRG_DIR ${LUA_PRG} PATH) + get_filename_component(LUA_PREFIX_DIR ${LUA_PRG_DIR} PATH) + file(GLOB_RECURSE BUSTED_REAL_PRG + ${LUA_PREFIX_DIR}/lib/luarocks/rocks/busted/*busted) + + add_custom_target(test + COMMAND ${CMAKE_COMMAND} + -DBUSTED_PRG=${PROJECT_SOURCE_DIR}/scripts/run-functional-tests.py + -DBUSTED_REAL_PRG=${BUSTED_REAL_PRG} + -DWORKING_DIR=${CMAKE_CURRENT_SOURCE_DIR} + -DBUSTED_OUTPUT_TYPE=${BUSTED_OUTPUT_TYPE} + -DTEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/test + -DBUILD_DIR=${CMAKE_BINARY_DIR} + -DTEST_TYPE=functional + -P ${CMAKE_MODULE_PATH}/RunTests.cmake + DEPENDS nvim) endif() @@ -74,9 +74,11 @@ endif mkdir -p build touch $@ -test: | nvim +oldtest: | nvim +$(SINGLE_MAKE) -C src/nvim/testdir $(MAKEOVERRIDES) - PATH="$$(pwd)/build/bin:$$PATH" vroom --neovim --crawl test + +test: | nvim + +$(BUILD_CMD) -C build test unittest: | nvim +$(BUILD_CMD) -C build unittest diff --git a/cmake/RunTests.cmake b/cmake/RunTests.cmake new file mode 100644 index 0000000000..b89957bb28 --- /dev/null +++ b/cmake/RunTests.cmake @@ -0,0 +1,23 @@ +get_filename_component(BUSTED_DIR ${BUSTED_PRG} PATH) +set(ENV{PATH} "${BUSTED_DIR}:$ENV{PATH}") +if(DEFINED ENV{TEST_FILE}) + set(TEST_DIR $ENV{TEST_FILE}) +endif() + +if(TEST_TYPE STREQUAL "functional") + execute_process( + COMMAND python ${BUSTED_PRG} ${BUSTED_REAL_PRG} -v -o + ${BUSTED_OUTPUT_TYPE} --lpath=${BUILD_DIR}/?.lua ${TEST_DIR}/functional + WORKING_DIRECTORY ${WORKING_DIR} + RESULT_VARIABLE res) +else() + execute_process( + COMMAND ${BUSTED_PRG} -v -o ${BUSTED_OUTPUT_TYPE} + --lpath=${BUILD_DIR}/?.lua ${TEST_DIR}/unit + WORKING_DIRECTORY ${WORKING_DIR} + RESULT_VARIABLE res) +endif() + +if(NOT res EQUAL 0) + message(FATAL_ERROR "Unit tests failed.") +endif() diff --git a/cmake/RunUnittests.cmake b/cmake/RunUnittests.cmake deleted file mode 100644 index f51fc8a90a..0000000000 --- a/cmake/RunUnittests.cmake +++ /dev/null @@ -1,14 +0,0 @@ -get_filename_component(BUSTED_DIR ${BUSTED_PRG} PATH) -set(ENV{PATH} "${BUSTED_DIR}:$ENV{PATH}") -if(DEFINED ENV{TEST_FILE}) - set(TEST_DIR $ENV{TEST_FILE}) -endif() - -execute_process( - COMMAND ${BUSTED_PRG} -v -o ${BUSTED_OUTPUT_TYPE} --lpath=${BUILD_DIR}/?.lua ${TEST_DIR} - WORKING_DIRECTORY ${WORKING_DIR} - RESULT_VARIABLE res) - -if(NOT res EQUAL 0) - message(FATAL_ERROR "Unit tests failed.") -endif() diff --git a/scripts/run-functional-tests.py b/scripts/run-functional-tests.py new file mode 100644 index 0000000000..3e931b248c --- /dev/null +++ b/scripts/run-functional-tests.py @@ -0,0 +1,93 @@ +# Run functional tests using lua, busted and the python client + +import os +import sys +import textwrap + +from lupa import LuaRuntime, as_attrgetter +from neovim import Nvim, spawn_session + + +# Extract arguments +busted_script = sys.argv[1] +busted_argv = sys.argv[2:] + +# Setup a lua state for running busted +lua = LuaRuntime(unpack_returned_tuples=True) +lua_globals = lua.globals() + +# helper to transform iterables into lua tables +list_to_table = lua.eval(''' +function(l) + local t = {} + for i, item in python.enumerate(l) do t[i + 1] = item end + return t +end +''') + +dict_to_table = lua.eval(''' +function(d) + local t = {} + for k, v in python.iterex(d.items()) do t[k] = v end + return t +end +''') + +def to_table(obj): + if type(obj) in [tuple, list]: + return list_to_table(list(to_table(e) for e in obj)) + if type(obj) is dict: + return dict_to_table(as_attrgetter( + dict((k, to_table(v)) for k, v in obj.items()))) + return obj + +nvim_prog = os.environ.get('NVIM_PROG', 'build/bin/nvim') +nvim_argv = [nvim_prog, '-u', 'NONE', '--embed'] + +if 'VALGRIND' in os.environ: + log_file = os.environ.get('VALGRIND_LOG', 'valgrind-%p.log') + valgrind_argv = ['valgrind', '-q', '--tool=memcheck', '--leak-check=yes', + '--track-origins=yes', '--suppressions=.valgrind.supp', + '--log-file={0}'.format(log_file)] + if 'VALGRIND_GDB' in os.environ: + valgrind_argv += ['--vgdb=yes', '--vgdb-error=0'] + nvim_argv = valgrind_argv + nvim_argv + +session = spawn_session(nvim_argv) +nvim = Nvim.from_session(session) + +def nvim_command(cmd): + nvim.command(cmd) + +def nvim_eval(expr): + return to_table(nvim.eval(expr)) + +def nvim_feed(input, mode=''): + nvim.feedkeys(input) + +def buffer_slice(start=None, stop=None, buffer_idx=None): + rv = '\n'.join(nvim.buffers[buffer_idx or 0][start:stop]) + return rv + +def nvim_replace_termcodes(input, *opts): + return nvim.replace_termcodes(input, *opts) + +expose = [ + nvim_command, + nvim_eval, + nvim_feed, + nvim_replace_termcodes, + buffer_slice, + textwrap.dedent, +] + +for fn in expose: + lua_globals[fn.__name__] = fn + +# Set 'arg' global to let busted parse arguments +lua_globals['arg'] = list_to_table(busted_argv) + +# Read the busted script and execute in the lua state +with open(busted_script) as f: + busted_setup = f.read() +lua.execute(busted_setup) diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index 62559163d8..4ff5845bd4 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -31,7 +31,7 @@ /// @param buffer The buffer handle /// @param[out] err Details of an error that may have occurred /// @return The line count -Integer buffer_get_length(Buffer buffer, Error *err) +Integer buffer_line_count(Buffer buffer, Error *err) { buf_T *buf = find_buffer_by_handle(buffer, err); @@ -51,7 +51,7 @@ Integer buffer_get_length(Buffer buffer, Error *err) String buffer_get_line(Buffer buffer, Integer index, Error *err) { String rv = {.size = 0}; - Array slice = buffer_get_slice(buffer, index, index, true, true, err); + Array slice = buffer_get_line_slice(buffer, index, index, true, true, err); if (!err->set && slice.size) { rv = slice.items[0].data.string; @@ -72,7 +72,7 @@ void buffer_set_line(Buffer buffer, Integer index, String line, Error *err) { Object l = STRING_OBJ(line); Array array = {.items = &l, .size = 1}; - buffer_set_slice(buffer, index, index, true, true, array, err); + buffer_set_line_slice(buffer, index, index, true, true, array, err); } /// Deletes a buffer line @@ -83,7 +83,7 @@ void buffer_set_line(Buffer buffer, Integer index, String line, Error *err) void buffer_del_line(Buffer buffer, Integer index, Error *err) { Array array = ARRAY_DICT_INIT; - buffer_set_slice(buffer, index, index, true, true, array, err); + buffer_set_line_slice(buffer, index, index, true, true, array, err); } /// Retrieves a line range from the buffer @@ -95,7 +95,7 @@ void buffer_del_line(Buffer buffer, Integer index, Error *err) /// @param include_end True if the slice includes the `end` parameter /// @param[out] err Details of an error that may have occurred /// @return An array of lines -ArrayOf(String) buffer_get_slice(Buffer buffer, +ArrayOf(String) buffer_get_line_slice(Buffer buffer, Integer start, Integer end, Boolean include_start, @@ -156,7 +156,7 @@ end: /// @param replacement An array of lines to use as replacement(A 0-length // array will simply delete the line range) /// @param[out] err Details of an error that may have occurred -void buffer_set_slice(Buffer buffer, +void buffer_set_line_slice(Buffer buffer, Integer start, Integer end, Boolean include_start, @@ -445,7 +445,7 @@ void buffer_insert(Buffer buffer, ArrayOf(String) lines, Error *err) { - buffer_set_slice(buffer, lnum, lnum, false, true, lines, err); + buffer_set_line_slice(buffer, lnum, lnum, false, true, lines, err); } /// Return a tuple (row,col) representing the position of the named mark diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index b3bcf54258..6ce3c77c66 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -318,7 +318,7 @@ close_buffer ( } else if (buf->b_p_bh[0] == 'u') /* 'bufhidden' == "unload" */ unload_buf = true; - if (win != NULL) { + if (win_valid(win)) { /* Set b_last_cursor when closing the last window for the buffer. * Remember the last cursor position and window options of the buffer. * This used to be only for the current window, but then options like diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 3d0607d8f7..8b02a4187a 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -4686,12 +4686,13 @@ list_T *list_alloc(void) FUNC_ATTR_NONNULL_RET /* * Allocate an empty list for a return value. */ -static void rettv_list_alloc(typval_T *rettv) +static list_T *rettv_list_alloc(typval_T *rettv) { list_T *l = list_alloc(); rettv->vval.v_list = l; rettv->v_type = VAR_LIST; ++l->lv_refcount; + return l; } /* @@ -6543,6 +6544,7 @@ static struct fst { {"synconcealed", 2, 2, f_synconcealed}, {"synstack", 2, 2, f_synstack}, {"system", 1, 2, f_system}, + {"systemlist", 1, 2, f_systemlist}, {"tabpagebuflist", 0, 1, f_tabpagebuflist}, {"tabpagenr", 0, 1, f_tabpagenr}, {"tabpagewinnr", 1, 2, f_tabpagewinnr}, @@ -9407,16 +9409,15 @@ static void f_getpid(typval_T *argvars, typval_T *rettv) static void getpos_both(typval_T *argvars, typval_T *rettv, bool getcurpos) { pos_T *fp; - list_T *l; int fnum = -1; - rettv_list_alloc(rettv); - l = rettv->vval.v_list; if (getcurpos) { fp = &curwin->w_cursor; } else { fp = var2fpos(&argvars[0], true, &fnum); } + + list_T *l = rettv_list_alloc(rettv); list_append_number(l, (fnum != -1) ? (varnumber_T)fnum : (varnumber_T)0); list_append_number(l, (fp != NULL) ? (varnumber_T)fp->lnum : (varnumber_T)0); list_append_number(l, @@ -9873,7 +9874,7 @@ static void f_has(typval_T *argvars, typval_T *rettv) "spell", "syntax", #if !defined(UNIX) - "system", + "system", // TODO(SplinterOfChaos): This IS defined for UNIX! #endif "tag_binary", "tag_old_static", @@ -12009,8 +12010,7 @@ static void f_remove(typval_T *argvars, typval_T *rettv) EMSG(_(e_invrange)); else { vim_list_remove(l, item, item2); - rettv_list_alloc(rettv); - l = rettv->vval.v_list; + l = rettv_list_alloc(rettv); l->lv_first = item; l->lv_last = item2; item->li_prev = NULL; @@ -13461,15 +13461,36 @@ static int item_compare(const void *s1, const void *s2) { sortItem_T *si1, *si2; char_u *p1, *p2; - char_u *tofree1, *tofree2; + char_u *tofree1 = NULL, *tofree2 = NULL; int res; char_u numbuf1[NUMBUFLEN]; char_u numbuf2[NUMBUFLEN]; si1 = (sortItem_T *)s1; si2 = (sortItem_T *)s2; - p1 = tv2string(&si1->item->li_tv, &tofree1, numbuf1, 0); - p2 = tv2string(&si2->item->li_tv, &tofree2, numbuf2, 0); + typval_T *tv1 = &si1->item->li_tv; + typval_T *tv2 = &si2->item->li_tv; + // tv2string() puts quotes around a string and allocates memory. Don't do + // that for string variables. Use a single quote when comparing with a + // non-string to do what the docs promise. + if (tv1->v_type == VAR_STRING) { + if (tv2->v_type != VAR_STRING || item_compare_numeric) { + p1 = (char_u *)"'"; + } else { + p1 = tv1->vval.v_string; + } + } else { + p1 = tv2string(tv1, &tofree1, numbuf1, 0); + } + if (tv2->v_type == VAR_STRING) { + if (tv1->v_type != VAR_STRING || item_compare_numeric) { + p2 = (char_u *)"'"; + } else { + p2 = tv2->vval.v_string; + } + } else { + p2 = tv2string(tv2, &tofree2, numbuf2, 0); + } if (p1 == NULL) p1 = (char_u *)""; if (p2 == NULL) @@ -13487,8 +13508,8 @@ static int item_compare(const void *s1, const void *s2) res = n1 == n2 ? 0 : n1 > n2 ? 1 : -1; } - // When the result would be zero, compare the pointers themselves. Makes - // the sort stable. + // When the result would be zero, compare the item indexes. Makes the + // sort stable. if (res == 0 && !item_compare_keep_zero) { res = si1->idx > si2->idx ? 1 : -1; } @@ -14394,8 +14415,8 @@ static void f_synstack(typval_T *argvars, typval_T *rettv) } } -/// f_system - the VimL system() function -static void f_system(typval_T *argvars, typval_T *rettv) +static void get_system_output_as_rettv(typval_T *argvars, typval_T *rettv, + bool retlist) { rettv->v_type = VAR_STRING; rettv->vval.v_string = NULL; @@ -14405,10 +14426,11 @@ static void f_system(typval_T *argvars, typval_T *rettv) } // get input to the shell command (if any), and its length - char_u buf[NUMBUFLEN]; - const char *input = (argvars[1].v_type != VAR_UNKNOWN) - ? (char *) get_tv_string_buf_chk(&argvars[1], buf): NULL; - size_t input_len = input ? strlen(input) : 0; + ssize_t input_len; + char *input = (char *) save_tv_as_string(&argvars[1], &input_len); + if (input_len == -1) { + return; + } // get shell command to execute const char *cmd = (char *) get_tv_string(&argvars[0]); @@ -14418,11 +14440,40 @@ static void f_system(typval_T *argvars, typval_T *rettv) char *res = NULL; int status = os_system(cmd, input, input_len, &res, &nread); + free(input); + set_vim_var_nr(VV_SHELL_ERROR, (long) status); + if (res == NULL) { + return; + } + + if (retlist) { + list_T *list = rettv_list_alloc(rettv); + + // Copy each line to a list element using NL as the delimiter. + for (size_t i = 0; i < nread; i++) { + char_u *start = (char_u *) res + i; + size_t len = (char_u *) xmemscan(start, NL, nread - i) - start; + i += len; + + // Don't use a str function to copy res as it may contains NULs. + char_u *s = xmemdupz(start, len); + memchrsub(s, NUL, NL, len); // Replace NUL with NL to avoid truncation. + + listitem_T *li = listitem_alloc(); + li->li_tv.v_type = VAR_STRING; + li->li_tv.vval.v_string = s; + list_append(list, li); + } + + free(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> - if (res != NULL) { + // translate <CR><NL> into <NL> char *d = res; for (char *s = res; *s; ++s) { if (s[0] == CAR && s[1] == NL) { @@ -14433,12 +14484,23 @@ static void f_system(typval_T *argvars, typval_T *rettv) } *d = NUL; - } #endif + rettv->vval.v_string = (char_u *) res; + } +} - rettv->vval.v_string = (char_u *) res; +/// f_system - the VimL system() function +static void f_system(typval_T *argvars, typval_T *rettv) +{ + get_system_output_as_rettv(argvars, rettv, false); +} + +static void f_systemlist(typval_T *argvars, typval_T *rettv) +{ + get_system_output_as_rettv(argvars, rettv, true); } + /* * "tabpagebuflist()" function */ @@ -14579,8 +14641,7 @@ static void f_taglist(typval_T *argvars, typval_T *rettv) if (*tag_pattern == NUL) return; - rettv_list_alloc(rettv); - (void)get_tags(rettv->vval.v_list, tag_pattern); + (void)get_tags(rettv_list_alloc(rettv), tag_pattern); } /* @@ -15037,6 +15098,87 @@ static void f_winsaveview(typval_T *argvars, typval_T *rettv) dict_add_nr_str(dict, "skipcol", (long)curwin->w_skipcol, NULL); } +/// Writes list of strings to file +static bool write_list(FILE *fd, list_T *list, bool binary) +{ + int ret = true; + + for (listitem_T *li = list->lv_first; li != NULL; li = li->li_next) { + for (char_u *s = get_tv_string(&li->li_tv); *s != NUL; ++s) { + if (putc(*s == '\n' ? NUL : *s, fd) == EOF) { + ret = false; + break; + } + } + if (!binary || li->li_next != NULL) { + if (putc('\n', fd) == EOF) { + ret = false; + break; + } + } + if (ret == false) { + EMSG(_(e_write)); + break; + } + } + return ret; +} + +/// Saves a typval_T as a string. +/// +/// For lists, replaces NLs with NUL and separates items with NLs. +/// +/// @param[in] tv A value to store as a string. +/// @param[out] len The length of the resulting string or -1 on error. +/// @returns an allocated string if `tv` represents a VimL string, list, or +/// number; NULL otherwise. +static char_u *save_tv_as_string(typval_T *tv, ssize_t *len) + FUNC_ATTR_MALLOC FUNC_ATTR_NONNULL_ALL +{ + if (tv->v_type == VAR_UNKNOWN) { + *len = 0; + return NULL; + } + + // For types other than list, let get_tv_string_buf_chk() get the value or + // print an error. + if (tv->v_type != VAR_LIST) { + char_u *ret = get_tv_string_chk(tv); + if (ret && (*len = STRLEN(ret))) { + ret = vim_strsave(ret); + } else { + ret = NULL; + *len = -1; + } + return ret; + } + + // Pre-calculate the resulting length. + *len = 0; + list_T *list = tv->vval.v_list; + for (listitem_T *li = list->lv_first; li != NULL; li = li->li_next) { + *len += STRLEN(get_tv_string(&li->li_tv)) + 1; + } + + if (*len == 0) { + return NULL; + } + + char_u *ret = xmalloc(*len); + char_u *end = ret; + for (listitem_T *li = list->lv_first; li != NULL; li = li->li_next) { + for (char_u *s = get_tv_string(&li->li_tv); *s != NUL; s++) { + *end++ = (*s == '\n') ? NUL : *s; + } + if (li->li_next != NULL) { + *end++ = '\n'; + } + } + *end = NUL; + *len = end - ret; + return ret; +} + /* * "winwidth(nr)" function */ @@ -15051,68 +15193,43 @@ static void f_winwidth(typval_T *argvars, typval_T *rettv) rettv->vval.v_number = wp->w_width; } -/* - * "writefile()" function - */ +/// "writefile()" function static void f_writefile(typval_T *argvars, typval_T *rettv) { - int binary = FALSE; - char_u *fname; - FILE *fd; - listitem_T *li; - char_u *s; - int ret = 0; - int c; + rettv->vval.v_number = 0; // Assuming success. - if (check_restricted() || check_secure()) + if (check_restricted() || check_secure()) { return; + } if (argvars[0].v_type != VAR_LIST) { EMSG2(_(e_listarg), "writefile()"); return; } - if (argvars[0].vval.v_list == NULL) + if (argvars[0].vval.v_list == NULL) { return; + } + bool binary = false; if (argvars[2].v_type != VAR_UNKNOWN - && STRCMP(get_tv_string(&argvars[2]), "b") == 0) - binary = TRUE; + && STRCMP(get_tv_string(&argvars[2]), "b") == 0) { + binary = true; + } - /* Always open the file in binary mode, library functions have a mind of - * their own about CR-LF conversion. */ - fname = get_tv_string(&argvars[1]); + // Always open the file in binary mode, library functions have a mind of + // their own about CR-LF conversion. + char_u *fname = get_tv_string(&argvars[1]); + FILE *fd; if (*fname == NUL || (fd = mch_fopen((char *)fname, WRITEBIN)) == NULL) { EMSG2(_(e_notcreate), *fname == NUL ? (char_u *)_("<empty>") : fname); - ret = -1; + rettv->vval.v_number = -1; } else { - for (li = argvars[0].vval.v_list->lv_first; li != NULL; - li = li->li_next) { - for (s = get_tv_string(&li->li_tv); *s != NUL; ++s) { - if (*s == '\n') - c = putc(NUL, fd); - else - c = putc(*s, fd); - if (c == EOF) { - ret = -1; - break; - } - } - if (!binary || li->li_next != NULL) - if (putc('\n', fd) == EOF) { - ret = -1; - break; - } - if (ret < 0) { - EMSG(_(e_write)); - break; - } + if (write_list(fd, argvars[0].vval.v_list, binary) == false) { + rettv->vval.v_number = -1; } fclose(fd); } - - rettv->vval.v_number = ret; } - /* * "xor(expr, expr)" function */ diff --git a/src/nvim/ex_cmds2.c b/src/nvim/ex_cmds2.c index f826213d08..ee34d23dd2 100644 --- a/src/nvim/ex_cmds2.c +++ b/src/nvim/ex_cmds2.c @@ -3181,8 +3181,8 @@ static char_u **find_locales(void) /* Find all available locales by running command "locale -a". If this * doesn't work we won't have completion. */ - char_u *locale_a = get_cmd_output((char_u *)"locale -a", - NULL, kShellOptSilent); + char_u *locale_a = get_cmd_output((char_u *)"locale -a", NULL, + kShellOptSilent, NULL); if (locale_a == NULL) return NULL; ga_init(&locales_ga, sizeof(char_u *), 20); diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index 948580bff2..e9f1aad781 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -6750,7 +6750,8 @@ apply_autocmds_group ( --nesting; /* see matching increment above */ // When stopping to execute autocommands, restore the search patterns and - // the redo buffer. Free buffers in the au_pending_free_buf list. + // the redo buffer. Free any buffers in the au_pending_free_buf list and + // free any windows in the au_pending_free_win list. if (!autocmd_busy) { restore_search_patterns(); restoreRedobuff(); @@ -6760,6 +6761,11 @@ apply_autocmds_group ( free(au_pending_free_buf); au_pending_free_buf = b; } + while (au_pending_free_win != NULL) { + win_T *w = au_pending_free_win->w_next; + free(au_pending_free_win); + au_pending_free_win = w; + } } /* diff --git a/src/nvim/globals.h b/src/nvim/globals.h index d9c774439c..25c453fea8 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -468,10 +468,12 @@ EXTERN int keep_filetype INIT(= FALSE); /* value for did_filetype when * which one is preferred, au_new_curbuf is set to it */ EXTERN buf_T *au_new_curbuf INIT(= NULL); -// When deleting the buffer and autocmd_busy is TRUE, do not free the buffer -// but link it in the list starting with au_pending_free_buf, using b_next. -// Free the buffer when autocmd_busy is set to FALSE. +// When deleting a buffer/window and autocmd_busy is TRUE, do not free the +// buffer/window. but link it in the list starting with +// au_pending_free_buf/ap_pending_free_win, using b_next/w_next. +// Free the buffer/window when autocmd_busy is being set to FALSE. EXTERN buf_T *au_pending_free_buf INIT(= NULL); +EXTERN win_T *au_pending_free_win INIT(= NULL); /* * Mouse coordinates, set by check_termcode() diff --git a/src/nvim/memory.c b/src/nvim/memory.c index f4dffb0bcd..59edefec4a 100644 --- a/src/nvim/memory.c +++ b/src/nvim/memory.c @@ -1,5 +1,6 @@ // Various routines dealing with allocation and deallocation of memory. +#include <assert.h> #include <errno.h> #include <inttypes.h> #include <string.h> @@ -222,6 +223,66 @@ void *xmemdupz(const void *data, size_t len) return memcpy(xmallocz(len), data, len); } +/// A version of strchr() that returns a pointer to the terminating NUL if it +/// doesn't find `c`. +/// +/// @param str The string to search. +/// @param c The char to look for. +/// @returns a pointer to the first instance of `c`, or to the NUL terminator +/// if not found. +char *xstrchrnul(const char *str, char c) + FUNC_ATTR_NONNULL_RET FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PURE +{ + char *p = strchr(str, c); + return p ? p : (char *)(str + strlen(str)); +} + +/// A version of memchr() that returns a pointer one past the end +/// if it doesn't find `c`. +/// +/// @param addr The address of the memory object. +/// @param c The char to look for. +/// @param size The size of the memory object. +/// @returns a pointer to the first instance of `c`, or one past the end if not +/// found. +void *xmemscan(const void *addr, char c, size_t size) + FUNC_ATTR_NONNULL_RET FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PURE +{ + char *p = memchr(addr, c, size); + return p ? p : (char *)addr + size; +} + +/// Replaces every instance of `c` with `x`. +/// +/// @warning Will read past `str + strlen(str)` if `c == NUL`. +/// +/// @param str A NUL-terminated string. +/// @param c The unwanted byte. +/// @param x The replacement. +void strchrsub(char *str, char c, char x) + FUNC_ATTR_NONNULL_ALL +{ + assert(c != '\0'); + while ((str = strchr(str, c))) { + *str++ = x; + } +} + +/// Replaces every instance of `c` with `x`. +/// +/// @param data An object in memory. May contain NULs. +/// @param c The unwanted byte. +/// @param x The replacement. +/// @param len The length of data. +void memchrsub(void *data, char c, char x, size_t len) + FUNC_ATTR_NONNULL_ALL +{ + char *p = data, *end = (char *)data + len; + while ((p = memchr(p, c, (size_t)(end - p)))) { + *p++ = x; + } +} + /// The xstpcpy() function shall copy the string pointed to by src (including /// the terminating NUL character) into the array pointed to by dst. /// diff --git a/src/nvim/message.c b/src/nvim/message.c index ef0faa35ee..58dbee8cf9 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -3173,8 +3173,7 @@ int vim_vsnprintf(char *str, size_t str_m, char *fmt, va_list ap, typval_T *tvs) p = ""; while (*p != NUL) { if (*p != '%') { - char *q = strchr(p + 1, '%'); - size_t n = (q == NULL) ? STRLEN(p) : (size_t)(q - p); + size_t n = xstrchrnul(p + 1, '%') - p; /* Copy up to the next '%' or NUL without any changes. */ if (str_l < str_m) { diff --git a/src/nvim/misc1.c b/src/nvim/misc1.c index 770026ed88..2eea48df00 100644 --- a/src/nvim/misc1.c +++ b/src/nvim/misc1.c @@ -3401,13 +3401,16 @@ void fast_breakcheck(void) /* * Get the stdout of an external command. + * If "ret_len" is NULL replace NUL characters with NL. When "ret_len" is not + * NULL store the length there. * Returns an allocated string, or NULL for error. */ char_u * get_cmd_output ( char_u *cmd, char_u *infile, /* optional input file name */ - int flags /* can be SHELL_SILENT */ + int flags, // can be kShellOptSilent + size_t *ret_len ) { char_u *tempname; @@ -3463,13 +3466,15 @@ get_cmd_output ( EMSG2(_(e_notread), tempname); free(buffer); buffer = NULL; - } else { + } else if (ret_len == NULL) { /* Change NUL into SOH, otherwise the string is truncated. */ for (i = 0; i < len; ++i) if (buffer[i] == NUL) buffer[i] = 1; buffer[len] = NUL; /* make sure the buffer is terminated */ + } else { + *ret_len = len; } done: diff --git a/src/nvim/ops.c b/src/nvim/ops.c index ad37b2374f..ed2a82d55c 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -4903,10 +4903,9 @@ static void str_to_reg(struct yankreg *y_ptr, * Find the end of each line and save it into the array. */ for (start = 0; start < len + extraline; start += i + 1) { - for (i = start; i < len; ++i) /* find the end of the line */ - if (str[i] == '\n') - break; - i -= start; /* i is now length of line */ + // Let i represent the length of one line. + const char_u *p = str + start; + i = (char_u *)xmemscan(p, '\n', len - start) - p; if (i > maxlen) maxlen = i; if (append) { diff --git a/src/nvim/os/fs.c b/src/nvim/os/fs.c index 07accb339a..36c2bb6d9b 100644 --- a/src/nvim/os/fs.c +++ b/src/nvim/os/fs.c @@ -134,10 +134,7 @@ static bool is_executable_in_path(const char_u *name, char_u **abspath) // Walk through all entries in $PATH to check if "name" exists there and // is an executable file. for (;; ) { - const char *e = strchr(path, ':'); - if (e == NULL) { - e = path + STRLEN(path); - } + const char *e = xstrchrnul(path, ':'); // Glue together the given directory from $PATH with name and save into // buf. diff --git a/src/nvim/os/shell.c b/src/nvim/os/shell.c index 398d94b606..912dc95aca 100644 --- a/src/nvim/os/shell.c +++ b/src/nvim/os/shell.c @@ -293,19 +293,15 @@ int os_system(const char *cmd, if (input) { WBuffer *input_buffer = wstream_new_buffer((char *) input, len, 1, NULL); - // we want to be notified when the write completes - job_write_cb(job, system_write_cb); - if (!job_write(job, input_buffer)) { // couldn't write, stop the job and tell the user about it job_stop(job); return -1; } - } else { - // close the input stream, let the process know that no input is coming - job_close_in(job); } + // close the input stream, let the process know that no more input is coming + job_close_in(job); int status = job_wait(job, -1); // prepare the out parameters if requested @@ -353,17 +349,6 @@ static void system_data_cb(RStream *rstream, void *data, bool eof) buf->len += nread; } -static void system_write_cb(WStream *wstream, - void *data, - size_t pending, - int status) -{ - if (pending == 0) { - Job *job = data; - job_close_in(job); - } -} - /// Parses a command string into a sequence of words, taking quotes into /// consideration. /// diff --git a/src/nvim/os/wstream.c b/src/nvim/os/wstream.c index 00a53d1628..eb7de02a2f 100644 --- a/src/nvim/os/wstream.c +++ b/src/nvim/os/wstream.c @@ -208,15 +208,14 @@ static void write_cb(uv_write_t *req, int status) release_wbuffer(data->buffer); - data->wstream->pending_reqs--; - if (data->wstream->cb) { data->wstream->cb(data->wstream, data->wstream->data, - data->wstream->pending_reqs, status); } + data->wstream->pending_reqs--; + if (data->wstream->freed && data->wstream->pending_reqs == 0) { // Last pending write, free the wstream; free(data->wstream); diff --git a/src/nvim/os/wstream_defs.h b/src/nvim/os/wstream_defs.h index e42481f283..cfa0bf0b60 100644 --- a/src/nvim/os/wstream_defs.h +++ b/src/nvim/os/wstream_defs.h @@ -10,11 +10,9 @@ typedef void (*wbuffer_data_finalizer)(void *data); /// /// @param wstream The `WStream` instance /// @param data User-defined data -/// @param pending The number of write requests that are still pending /// @param status 0 on success, anything else indicates failure typedef void (*wstream_cb)(WStream *wstream, void *data, - size_t pending, int status); #endif // NVIM_OS_WSTREAM_DEFS_H diff --git a/src/nvim/path.c b/src/nvim/path.c index 6990a1817c..ff97b7774a 100644 --- a/src/nvim/path.c +++ b/src/nvim/path.c @@ -1172,7 +1172,7 @@ expand_backtick ( buffer = eval_to_string(cmd + 1, &p, TRUE); else buffer = get_cmd_output(cmd, NULL, - (flags & EW_SILENT) ? kShellOptSilent : 0); + (flags & EW_SILENT) ? kShellOptSilent : 0, NULL); free(cmd); if (buffer == NULL) return 0; diff --git a/src/nvim/screen.c b/src/nvim/screen.c index a4e38aa845..4e6123f206 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -346,7 +346,7 @@ int redraw_asap(int type) * Note that when also inserting/deleting lines w_redraw_top and w_redraw_bot * may become invalid and the whole window will have to be redrawn. */ -void +void redrawWinline ( linenr_T lnum, int invalid /* window line height is invalid now */ @@ -2114,7 +2114,7 @@ static void copy_text_attr(int off, char_u *buf, int len, int attr) * Fill the foldcolumn at "p" for window "wp". * Only to be called when 'foldcolumn' > 0. */ -static void +static void fill_foldcolumn ( char_u *p, win_T *wp, @@ -2166,7 +2166,7 @@ fill_foldcolumn ( * * Return the number of last row the line occupies. */ -static int +static int win_line ( win_T *wp, linenr_T lnum, @@ -2655,8 +2655,8 @@ win_line ( } next_search_hl(wp, shl, lnum, (colnr_T)v, cur); - // Need to get the line again, a multi-line regexp may have made it - // invalid. + // Need to get the line again, a multi-line regexp may have made it + // invalid. line = ml_get_buf(wp->w_buffer, lnum, false); ptr = line + v; @@ -2672,7 +2672,7 @@ win_line ( } else { shl->endcol = MAXCOL; } - // Highlight one character for an empty match. + // Highlight one character for an empty match. if (shl->startcol == shl->endcol) { if (has_mbyte && line[shl->endcol] != NUL) { shl->endcol += (*mb_ptr2len)(line + shl->endcol); @@ -2680,7 +2680,7 @@ win_line ( ++shl->endcol; } } - if ((long)shl->startcol < v) { // match at leftcol + if ((long)shl->startcol < v) { // match at leftcol shl->attr_cur = shl->attr; search_attr = shl->attr; } @@ -2830,9 +2830,12 @@ win_line ( if (wp->w_p_bri && n_extra == 0 && row != startrow && filler_lines == 0) { char_attr = 0; // was: hl_attr(HLF_AT); - if (diff_hlf != (hlf_T)0) + if (diff_hlf != (hlf_T)0) { char_attr = hl_attr(diff_hlf); - + if (wp->w_p_cul && lnum == wp->w_cursor.lnum) { + char_attr = hl_combine_attr(char_attr, hl_attr(HLF_CUL)); + } + } p_extra = NULL; c_extra = ' '; n_extra = get_breakindent_win(wp, ml_get_buf(wp->w_buffer, lnum, FALSE)); @@ -2869,8 +2872,9 @@ win_line ( if (tocol == vcol) tocol += n_extra; /* combine 'showbreak' with 'cursorline' */ - if (wp->w_p_cul && lnum == wp->w_cursor.lnum) - char_attr = hl_combine_attr(char_attr, HLF_CLN); + if (wp->w_p_cul && lnum == wp->w_cursor.lnum) { + char_attr = hl_combine_attr(char_attr, hl_attr(HLF_CUL)); + } } } @@ -3016,6 +3020,9 @@ win_line ( && n_extra == 0) diff_hlf = HLF_CHD; /* changed line */ line_attr = hl_attr(diff_hlf); + if (wp->w_p_cul && lnum == wp->w_cursor.lnum) { + line_attr = hl_combine_attr(line_attr, hl_attr(HLF_CUL)); + } } /* Decide which of the highlight attributes to use. */ @@ -3620,8 +3627,12 @@ win_line ( char_attr = line_attr; if (diff_hlf == HLF_TXD) { diff_hlf = HLF_CHD; - if (attr == 0 || char_attr != attr) + if (attr == 0 || char_attr != attr) { char_attr = hl_attr(diff_hlf); + if (wp->w_p_cul && lnum == wp->w_cursor.lnum) { + char_attr = hl_combine_attr(char_attr, hl_attr(HLF_CUL)); + } + } } } } @@ -4676,7 +4687,7 @@ static int skip_status_match_char(expand_T *xp, char_u *s) * * If inversion is possible we use it. Else '=' characters are used. */ -void +void win_redr_status_matches ( expand_T *xp, int num_matches, @@ -4962,7 +4973,7 @@ void win_redr_status(win_T *wp) fillchar = fillchar_status(&attr, wp == curwin); else fillchar = fillchar_vsep(&attr); - screen_putchar(fillchar, wp->w_winrow + wp->w_height, + screen_putchar(fillchar, wp->w_winrow + wp->w_height, W_ENDCOL(wp), attr); } busy = FALSE; @@ -5024,7 +5035,7 @@ int stl_connected(win_T *wp) /* * Get the value to show for the language mappings, active 'keymap'. */ -int +int get_keymap_str ( win_T *wp, char_u *buf, /* buffer for the result */ @@ -5068,7 +5079,7 @@ get_keymap_str ( * Redraw the status line or ruler of window "wp". * When "wp" is NULL redraw the tab pages line from 'tabline'. */ -static void +static void win_redr_custom ( win_T *wp, int draw_ruler /* TRUE or FALSE */ @@ -5606,7 +5617,7 @@ static void prepare_search_hl(win_T *wp, linenr_T lnum) if (cur != NULL) { cur->pos.cur = 0; } - bool pos_inprogress = true; // mark that a position match search is + bool pos_inprogress = true; // mark that a position match search is // in progress n = 0; while (shl->first_lnum < lnum && (shl->rm.regprog != NULL @@ -5637,7 +5648,7 @@ static void prepare_search_hl(win_T *wp, linenr_T lnum) * shl->lnum is zero. * Careful: Any pointers for buffer lines will become invalid. */ -static void +static void next_search_hl ( win_T *win, match_T *shl, /* points to search_hl or a match */ @@ -5705,9 +5716,9 @@ next_search_hl ( if (shl->rm.regprog != NULL) { nmatched = vim_regexec_multi(&shl->rm, win, shl->buf, lnum, matchcol, &(shl->tm)); if (called_emsg || got_int) { - // Error while handling regexp: stop using this regexp. + // Error while handling regexp: stop using this regexp. if (shl == &search_hl) { - // don't free regprog in the match list, it's a copy + // don't free regprog in the match list, it's a copy vim_regfree(shl->rm.regprog); SET_NO_HLSEARCH(TRUE); } @@ -5735,11 +5746,11 @@ next_search_hl ( static int next_search_hl_pos( - match_T *shl, // points to a match + match_T *shl, // points to a match linenr_T lnum, - posmatch_T *posmatch, // match positions + posmatch_T *posmatch, // match positions colnr_T mincol // minimal column for a match -) +) { int i; int bot = -1; @@ -5754,8 +5765,8 @@ next_search_hl_pos( } if (posmatch->pos[i].lnum == lnum) { if (shl->lnum == lnum) { - // partially sort positions by column numbers - // on the same line + // partially sort positions by column numbers + // on the same line if (posmatch->pos[i].col < posmatch->pos[bot].col) { llpos_T tmp = posmatch->pos[i]; @@ -5772,7 +5783,7 @@ next_search_hl_pos( if (shl->lnum == lnum) { colnr_T start = posmatch->pos[bot].col == 0 ? 0: posmatch->pos[bot].col - 1; - colnr_T end = posmatch->pos[bot].col == 0 + colnr_T end = posmatch->pos[bot].col == 0 ? MAXCOL : start + posmatch->pos[bot].len; shl->rm.startpos[0].lnum = 0; @@ -7091,7 +7102,7 @@ static void win_rest_invalid(win_T *wp) * * return FAIL for failure, OK for success. */ -int +int screen_ins_lines ( int off, int row, @@ -7264,7 +7275,7 @@ screen_ins_lines ( * * Return OK for success, FAIL if the lines are not deleted. */ -int +int screen_del_lines ( int off, int row, @@ -7630,7 +7641,7 @@ void unshowmode(int force) static void draw_tabline(void) { int tabcount = 0; - int tabwidth; + int tabwidth = 0; int col = 0; int scol = 0; int attr; @@ -7676,9 +7687,13 @@ static void draw_tabline(void) ++tabcount; } - tabwidth = (Columns - 1 + tabcount / 2) / tabcount; - if (tabwidth < 6) + if (tabcount > 0) { + tabwidth = (Columns - 1 + tabcount / 2) / tabcount; + } + + if (tabwidth < 6) { tabwidth = 6; + } attr = attr_nosel; tabcount = 0; @@ -7720,8 +7735,7 @@ static void draw_tabline(void) if (col + len >= Columns - 3) break; screen_puts_len(NameBuff, len, 0, col, - hl_combine_attr(attr, hl_attr(HLF_T)) - ); + hl_combine_attr(attr, hl_attr(HLF_T))); col += len; } if (modified) diff --git a/src/nvim/testdir/Makefile b/src/nvim/testdir/Makefile index 81dc49e800..9f04f880b5 100644 --- a/src/nvim/testdir/Makefile +++ b/src/nvim/testdir/Makefile @@ -33,6 +33,7 @@ SCRIPTS := test_autoformat_join.out \ test106.out test107.out \ test_options.out \ test_listlbr.out test_listlbr_utf8.out \ + test_changelist.out \ test_breakindent.out \ test_insertcount.out diff --git a/src/nvim/testdir/test_changelist.in b/src/nvim/testdir/test_changelist.in new file mode 100644 index 0000000000..6c7c4306c3 --- /dev/null +++ b/src/nvim/testdir/test_changelist.in @@ -0,0 +1,22 @@ +Test changelist position after splitting window +Set 'undolevels' to make changelist for sourced file + +STARTTEST +:so small.vim +Gkylp:set ul=100 +Gylp:set ul=100 +gg +:vsplit +:try +: normal g; +: normal ggVGcpass +:catch +: normal ggVGcfail +:finally +: %w! test.out +:endtry +:qa! +ENDTEST + +1 +2 diff --git a/src/nvim/testdir/test_changelist.ok b/src/nvim/testdir/test_changelist.ok new file mode 100644 index 0000000000..2ae28399f5 --- /dev/null +++ b/src/nvim/testdir/test_changelist.ok @@ -0,0 +1 @@ +pass diff --git a/src/nvim/version.c b/src/nvim/version.c index 199c034e97..47f4a2f805 100644 --- a/src/nvim/version.c +++ b/src/nvim/version.c @@ -184,6 +184,19 @@ static char *(features[]) = { }; static int included_patches[] = { + //457, + //456, + //455, + 454, + //453, + //452, + //451, + //450, + //449, + //448, + //447, + //446, + //445, //444, //443, //442, @@ -217,7 +230,7 @@ static int included_patches[] = { //414, //413 NA //412 NA - //411, + 411, 410, //409 NA //408, @@ -237,12 +250,12 @@ static int included_patches[] = { //394, //393, 392, - //391, + 391, //390, //389, 388, 387, - //386, + 386, //385, //384 NA 383, @@ -308,7 +321,7 @@ static int included_patches[] = { 323, //322 NA //321 NA - //320, + 320, //319 NA 318, 317, @@ -372,7 +385,7 @@ static int included_patches[] = { //259 NA //258 NA //257 NA - //256, + 256, //255, //254, 253, @@ -380,8 +393,8 @@ static int included_patches[] = { 251, //250 NA //249, - //248, - //247, + 248, + 247, //246, 245, //244, diff --git a/src/nvim/window.c b/src/nvim/window.c index bce54eecdc..fe40cd2a95 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -364,14 +364,16 @@ newwindow: /*FALLTHROUGH*/ case ']': case Ctrl_RSB: - CHECK_CMDWIN reset_VIsual_and_resel(); /* stop Visual mode */ + CHECK_CMDWIN + // Keep visual mode, can select words to use as a tag. if (Prenum) postponed_split = Prenum; else postponed_split = -1; + g_do_tagpreview = 0; - /* Execute the command right here, required when - * "wincmd ]" was used in a function. */ + // Execute the command right here, required when + // "wincmd ]" was used in a function. do_nv_ident(Ctrl_RSB, NUL); break; @@ -457,7 +459,7 @@ wingotofile: /*FALLTHROUGH*/ case ']': case Ctrl_RSB: - reset_VIsual_and_resel(); /* stop Visual mode */ + // Keep visual mode, can select words to use as a tag. if (Prenum) postponed_split = Prenum; else @@ -948,6 +950,9 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir) p_wh = size; } + // Keep same changelist position in new window. + wp->w_changelistidx = oldwin->w_changelistidx; + /* * make the new window the current window */ @@ -3692,7 +3697,12 @@ win_free ( if (wp != aucmd_win) win_remove(wp, tp); - free(wp); + if (autocmd_busy) { + wp->w_next = au_pending_free_win; + au_pending_free_win = wp; + } else { + free(wp); + } unblock_autocmds(); } diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua new file mode 100644 index 0000000000..671e34e592 --- /dev/null +++ b/test/functional/helpers.lua @@ -0,0 +1,102 @@ +local function clear() + nvim_command('call BeforeEachTest()') +end + +local function feed(...) + for _, v in ipairs({...}) do + nvim_feed(nvim_replace_termcodes(dedent(v))) + end +end + +local function rawfeed(...) + for _, v in ipairs({...}) do + nvim_feed(dedent(v), 'nt') + end +end + +local function insert(...) + nvim_feed('i', 'nt') + rawfeed(...) + nvim_feed(nvim_replace_termcodes('<ESC>'), 'nt') +end + +local function execute(...) + for _, v in ipairs({...}) do + if v:sub(1, 1) ~= '/' then + -- not a search command, prefix with colon + nvim_feed(':', 'nt') + end + nvim_feed(v, 'nt') + nvim_feed(nvim_replace_termcodes('<CR>'), 'nt') + end +end + +local function eval(expr) + local status, result = pcall(function() return nvim_eval(expr) end) + if not status then + error('Failed to evaluate expression "' .. expr .. '"') + end + return result +end + +local function eq(expected, actual) + return assert.are.same(expected, actual) +end + +local function neq(expected, actual) + return assert.are_not.same(expected, actual) +end + +local function expect(contents, first, last, buffer_index) + return eq(dedent(contents), buffer_slice(first, last, buffer_idx)) +end + +rawfeed([[:function BeforeEachTest() + set all& + redir => groups + silent augroup + redir END + for group in split(groups) + exe 'augroup '.group + autocmd! + augroup END + endfor + autocmd! + tabnew + let curbufnum = eval(bufnr('%')) + redir => buflist + silent ls! + redir END + let bufnums = [] + for buf in split(buflist, '\n') + let bufnum = eval(split(buf, '[ u]')[0]) + if bufnum != curbufnum + call add(bufnums, bufnum) + endif + endfor + if len(bufnums) > 0 + exe 'silent bwipeout! '.join(bufnums, ' ') + endif + silent tabonly + for k in keys(g:) + exe 'unlet g:'.k + endfor + filetype plugin indent off + mapclear + mapclear! + abclear + comclear +endfunction +]]) + +return { + clear = clear, + rawfeed = rawfeed, + insert = insert, + feed = feed, + execute = execute, + eval = eval, + eq = eq, + neq = neq, + expect = expect +} diff --git a/test/functional/legacy/002_filename_recognition_spec.lua b/test/functional/legacy/002_filename_recognition_spec.lua new file mode 100644 index 0000000000..569e748631 --- /dev/null +++ b/test/functional/legacy/002_filename_recognition_spec.lua @@ -0,0 +1,42 @@ +-- Test if URLs are recognized as filenames by commands such as "gf". Here +-- we'll use `expand("<cfile>")` since "gf" would need to open the file. + +local helpers = require('test.functional.helpers') +local clear, feed, insert = helpers.clear, helpers.feed, helpers.insert +local execute, expect = helpers.execute, helpers.expect + +describe('filename recognition', function() + setup(clear) + + it('is working', function() + -- insert some lines containing URLs + insert([[ + first test for URL://machine.name/tmp/vimtest2a and other text + second test for URL://machine.name/tmp/vimtest2b. And other text + third test for URL:\\machine.name\vimtest2c and other text + fourth test for URL:\\machine.name\tmp\vimtest2d, and other text]]) + + -- Go to the first URL and append it to the beginning + execute('/^first', '/tmp', 'call append(0, expand("<cfile>"))') + + -- Repeat for the second URL + -- this time, navigate to the word "URL" instead of "tmp" + execute('/^second', '/URL', 'call append(1, expand("<cfile>"))') + + -- Repeat for the remaining URLs. This time, the 'isfname' option must be + -- set to allow '\' in filenames + execute('set isf=@,48-57,/,.,-,_,+,,,$,:,~,\\') + execute('/^third', '/name', 'call append(2, expand("<cfile>"))') + execute('/^fourth', '/URL', 'call append(3, expand("<cfile>"))') + + -- Delete the initial text, which now starts at line 5 + feed('5GdG') + + -- The buffer should now contain: + expect([[ + URL://machine.name/tmp/vimtest2a + URL://machine.name/tmp/vimtest2b + URL:\\machine.name\vimtest2c + URL:\\machine.name\tmp\vimtest2d]]) + end) +end) diff --git a/test/functional/shell/viml_system_spec.lua b/test/functional/shell/viml_system_spec.lua new file mode 100644 index 0000000000..effabe715c --- /dev/null +++ b/test/functional/shell/viml_system_spec.lua @@ -0,0 +1,125 @@ +-- Specs for +-- - `system()` +-- - `systemlist()` + +local helpers = require('test.functional.helpers') +local eq, clear, eval, feed = + helpers.eq, helpers.clear, helpers.eval, helpers.feed + + +local function create_file_with_nuls(name) + return function() + feed('ipart1<C-V>000part2<C-V>000part3<ESC>:w '..name..'<CR>') + end +end + +local function delete_file(name) + return function() + eval("delete('"..name.."')") + end +end + + +describe('system()', function() + before_each(clear) + + describe('passing no input', function() + it('returns the program output', function() + eq("echoed", eval('system("echo -n echoed")')) + end) + end) + + describe('passing input', function() + it('returns the program output', function() + eq("input", eval('system("cat -", "input")')) + end) + end) + + describe('passing number as input', function() + it('stringifies the input', function() + eq('1', eval('system("cat", 1)')) + end) + end) + + describe('with output containing NULs', function() + local fname = 'Xtest' + + setup(create_file_with_nuls(fname)) + teardown(delete_file(fname)) + + it('replaces NULs by SOH characters', function() + eq('part1\001part2\001part3\n', eval('system("cat '..fname..'")')) + end) + end) + + describe('passing list as input', function() + it('joins list items with linefeed characters', function() + eq('line1\nline2\nline3', + eval("system('cat -', ['line1', 'line2', 'line3'])")) + end) + + -- Notice that NULs are converted to SOH when the data is read back. This + -- is inconsistent and is a good reason for the existence of the + -- `systemlist()` function, where input and output map to the same + -- characters(see the following tests with `systemlist()` below) + describe('with linefeed characters inside list items', function() + it('converts linefeed characters to NULs', function() + eq('l1\001p2\nline2\001a\001b\nl3', + eval([[system('cat -', ["l1\np2", "line2\na\nb", 'l3'])]])) + end) + end) + + describe('with leading/trailing whitespace characters on items', function() + it('preserves whitespace, replacing linefeeds by NULs', function() + eq('line \nline2\001\n\001line3', + eval([[system('cat -', ['line ', "line2\n", "\nline3"])]])) + end) + end) + end) +end) + +describe('systemlist()', function() + -- behavior is similar to `system()` but it returns a list instead of a + -- string. + before_each(clear) + + describe('passing string with linefeed characters as input', function() + it('splits the output on linefeed characters', function() + eq({'abc', 'def', 'ghi'}, eval([[systemlist("cat -", "abc\ndef\nghi")]])) + end) + end) + + describe('with output containing NULs', function() + local fname = 'Xtest' + + setup(create_file_with_nuls(fname)) + teardown(delete_file(fname)) + + it('replaces NULs by newline characters', function() + eq({'part1\npart2\npart3'}, eval('systemlist("cat '..fname..'")')) + end) + end) + + describe('passing list as input', function() + it('joins list items with linefeed characters', function() + eq({'line1', 'line2', 'line3'}, + eval("systemlist('cat -', ['line1', 'line2', 'line3'])")) + end) + + -- Unlike `system()` which uses SOH to represent NULs, with `systemlist()` + -- input and ouput are the same + describe('with linefeed characters inside list items', function() + it('converts linefeed characters to NULs', function() + eq({'l1\np2', 'line2\na\nb', 'l3'}, + eval([[systemlist('cat -', ["l1\np2", "line2\na\nb", 'l3'])]])) + end) + end) + + describe('with leading/trailing whitespace characters on items', function() + it('preserves whitespace, replacing linefeeds by NULs', function() + eq({'line ', 'line2\n', '\nline3'}, + eval([[systemlist('cat -', ['line ', "line2\n", "\nline3"])]])) + end) + end) + end) +end) diff --git a/test/legacy/test2-filename-recognition.vroom b/test/legacy/test2-filename-recognition.vroom deleted file mode 100644 index b8169ec207..0000000000 --- a/test/legacy/test2-filename-recognition.vroom +++ /dev/null @@ -1,40 +0,0 @@ -Test if URLs are recognized as filenames by commands such as "gf". Here -we'll use `expand("<cfile>")` since "gf" would need to open the file. - -Insert some URLs: - - % first test for URL://machine.name/tmp/vimtest2a and other text<cr> - % second test for URL://machine.name/tmp/vimtest2b. And other text<cr> - % third test for URL:\\machine.name\vimtest2c and other text<cr> - % fourth test for URL:\\machine.name\tmp\vimtest2d, and other text - -Go to the first URL: - - :/^first<cr>/tmp - -Append the url to the beginning of the buffer: - - :call append(0, expand("<cfile>")) - -Repeat for the second URL, but navigate to the 'URL' word: - - :/^second<cr>/URL<cr>:call append(1, expand("<cfile>")) - -Repeat for the other two, but first the 'isfname' option must be set to -allow '\' in filenames - - :set isf=@,48-57,/,.,-,_,+,,,$,:,~,\ - :/^third<cr>/name<cr>:call append(2, expand("<cfile>")) - :/^fourth<cr>/URL<cr>:call append(3, expand("<cfile>")) - -Delete the initial text which starts at line 5: - - > 5GdG - -Result: - - URL://machine.name/tmp/vimtest2a - URL://machine.name/tmp/vimtest2b - URL:\\machine.name\vimtest2c - URL:\\machine.name\tmp\vimtest2d - @end |