diff options
author | ckelsel <ckelsel@hotmail.com> | 2017-08-17 08:20:48 +0800 |
---|---|---|
committer | ckelsel <ckelsel@hotmail.com> | 2017-08-17 08:20:48 +0800 |
commit | 673fc748237345914e0ad584bc4cba997c96a37e (patch) | |
tree | 7f55a3b3dd362fbbc59ff204287a0aae99089e5e | |
parent | 5d332084e1ca4c29b1ccb90ea44c3b861dac5701 (diff) | |
parent | 5b32bce73c93a64970afe0e92e0a8ba2fed88619 (diff) | |
download | rneovim-673fc748237345914e0ad584bc4cba997c96a37e.tar.gz rneovim-673fc748237345914e0ad584bc4cba997c96a37e.tar.bz2 rneovim-673fc748237345914e0ad584bc4cba997c96a37e.zip |
Merge remote-tracking branch 'upstream/master'
45 files changed, 2293 insertions, 274 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 594f631ba0..17e14bcbd0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -351,6 +351,11 @@ endif() find_package(LibVterm REQUIRED) include_directories(SYSTEM ${LIBVTERM_INCLUDE_DIRS}) +if(WIN32) + find_package(Winpty REQUIRED) + include_directories(SYSTEM ${WINPTY_INCLUDE_DIRS}) +endif() + option(CLANG_ASAN_UBSAN "Enable Clang address & undefined behavior sanitizer for nvim binary." OFF) option(CLANG_MSAN "Enable Clang memory sanitizer for nvim binary." OFF) option(CLANG_TSAN "Enable Clang thread sanitizer for nvim binary." OFF) @@ -8,7 +8,7 @@ [](https://travis-ci.org/neovim/neovim) [](https://ci.appveyor.com/project/neovim/neovim/branch/master) -[](https://coveralls.io/r/neovim/neovim) +[](https://codecov.io/gh/neovim/neovim) [](https://scan.coverity.com/projects/2227) [](https://neovim.io/doc/reports/clang) [](https://neovim.io/doc/reports/pvs) diff --git a/appveyor.yml b/appveyor.yml index edb679d223..ecea6c5fa3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,6 +2,10 @@ version: '{build}' configuration: - MINGW_64 - MINGW_32 +- MINGW_64-gcov +matrix: + allow_failures: + - configuration: MINGW_64-gcov install: [] build_script: - call ci\build.bat diff --git a/ci/after_success.sh b/ci/after_success.sh index 0215eb139b..388b6eb714 100755 --- a/ci/after_success.sh +++ b/ci/after_success.sh @@ -5,4 +5,5 @@ set -o pipefail if [[ -n "${GCOV}" ]]; then coveralls --gcov "$(which "${GCOV}")" --encoding iso-8859-1 || echo 'coveralls upload failed.' + bash <(curl -s https://codecov.io/bash) || echo 'codecov upload failed.' fi diff --git a/ci/build.bat b/ci/build.bat index c871c6b849..6eb22176a9 100644 --- a/ci/build.bat +++ b/ci/build.bat @@ -9,6 +9,10 @@ if "%CONFIGURATION%" == "MINGW_32" ( set ARCH=x86_64 set BITS=64 ) +if "%CONFIGURATION%" == "MINGW_64-gcov" ( + set USE_GCOV="-DUSE_GCOV=ON" +) + :: We cannot have sh.exe in the PATH (MinGW) set PATH=%PATH:C:\Program Files\Git\usr\bin;=% set PATH=C:\msys64\mingw%BITS%\bin;C:\Windows\System32;C:\Windows;%PATH% @@ -38,13 +42,17 @@ cd .. :: Build Neovim mkdir build cd build -cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUSTED_OUTPUT_TYPE=nvim -DGPERF_PRG="C:\msys64\usr\bin\gperf.exe" .. || goto :error +cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUSTED_OUTPUT_TYPE=nvim %USE_GCOV% -DGPERF_PRG="C:\msys64\usr\bin\gperf.exe" .. || goto :error mingw32-make VERBOSE=1 || goto :error bin\nvim --version || goto :error :: Functional tests mingw32-make functionaltest VERBOSE=1 || goto :error +if defined USE_GCOV ( + C:\msys64\usr\bin\bash -lc "cd /c/projects/neovim; bash <(curl -s https://codecov.io/bash) || echo 'codecov upload failed.'" +) + :: Build artifacts cpack -G ZIP -C RelWithDebInfo if defined APPVEYOR_REPO_TAG_NAME cpack -G NSIS -C RelWithDebInfo diff --git a/cmake/FindWinpty.cmake b/cmake/FindWinpty.cmake new file mode 100644 index 0000000000..8feafc58a8 --- /dev/null +++ b/cmake/FindWinpty.cmake @@ -0,0 +1,10 @@ +include(LibFindMacros) + +find_path(WINPTY_INCLUDE_DIR winpty.h) +set(WINPTY_INCLUDE_DIRS ${WINPTY_INCLUDE_DIR}) + +find_library(WINPTY_LIBRARY winpty) +find_program(WINPTY_AGENT_EXE winpty-agent.exe) +set(WINPTY_LIBRARIES ${WINPTY_LIBRARY}) + +find_package_handle_standard_args(Winpty DEFAULT_MSG WINPTY_LIBRARY WINPTY_INCLUDE_DIR) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..0b3de06b97 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,27 @@ +codecov: + notify: + require_ci_to_pass: yes + ci: + - appveyor + - travis + - !neovim-qb.szakmeister.net + +coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: yes + patch: yes + changes: no + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: off diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index dd8aec895d..29e254b0b3 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -4703,6 +4703,7 @@ input({opts}) cancelreturn "" Same as {cancelreturn} from |inputdialog()|. Also works with input(). + highlight nothing Highlight handler: |Funcref|. The highlighting set with |:echohl| is used for the prompt. The input is entered just like a command-line, with the same @@ -4725,7 +4726,35 @@ input({opts}) "-complete=" argument. Refer to |:command-completion| for more information. Example: > let fname = input("File: ", "", "file") -< +< *E5400* *E5402* + The optional `highlight` key allows specifying function which + will be used for highlighting user input. This function + receives user input as its only argument and must return + a list of 3-tuples [hl_start_col, hl_end_col + 1, hl_group] + where + hl_start_col is the first highlighted column, + hl_end_col is the last highlighted column (+ 1!), + hl_group is |:hl| group used for highlighting. + *E5403* *E5404* *E5405* *E5406* + Both hl_start_col and hl_end_col + 1 must point to the start + of the multibyte character (highlighting must not break + multibyte characters), hl_end_col + 1 may be equal to the + input length. Start column must be in range [0, len(input)), + end column must be in range (hl_start_col, len(input)], + sections must be ordered so that next hl_start_col is greater + then or equal to previous hl_end_col. + + Highlight function is called at least once for each new + displayed input string, before command-line is redrawn. It is + expected that function is pure for the duration of one input() + call, i.e. it produces the same output for the same input, so + output may be memoized. Function is run like under |:silent| + modifier. If the function causes any errors, it will be + skipped for the duration of the current input() call. + + Currently coloring is disabled when command-line contains + arabic characters. + NOTE: This function must not be used in a startup file, for the versions that only run in GUI mode (e.g., the Win32 GUI). Note: When input() is called from within a mapping it will diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 7f1e5ce543..2f031c0b1f 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -129,7 +129,6 @@ Commands: Functions: |dictwatcheradd()| notifies a callback whenever a |Dict| is modified |dictwatcherdel()| - |execute()| works with |:redir| |menu_get()| |msgpackdump()|, |msgpackparse()| provide msgpack de/serialization @@ -147,6 +146,14 @@ Highlight groups: |hl-TermCursorNC| |hl-Whitespace| highlights 'listchars' whitespace +UI: + *E5408* *E5409* *g:Nvim_color_expr* *g:Nvim_color_cmdline* + Command-line coloring is supported. Only |input()| and |inputdialog()| may + be colored. For testing purposes expressions (e.g. |i_CTRL-R_=|) and regular + command-line (|:|) are colored by callbacks defined in `g:Nvim_color_expr` + and `g:Nvim_color_cmdline` respectively (these callbacks are for testing + only, and will be removed in a future version). + ============================================================================== 4. Changed features *nvim-features-changed* @@ -174,6 +181,8 @@ one. It does not attempt to mix data from the two. |system()| does not support writing/reading "backgrounded" commands. |E5677| +|:redir| nested in |execute()| works. + Nvim may throttle (skip) messages from shell commands (|:!|, |:grep|, |:make|) if there is too much output. No data is lost, this only affects display and makes things faster. |:terminal| output is never throttled. @@ -265,6 +274,8 @@ Lua interface (|if_lua.txt|): on cancel and completion respectively) via dictionary argument (replaces all other arguments if used). +|input()| and |inputdialog()| now support user-defined cmdline highlighting. + ============================================================================== 5. Missing legacy features *nvim-features-missing* diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index c46c0bed6d..688912eda6 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -111,6 +111,9 @@ foreach(sfile ${NVIM_SOURCES}) if(WIN32 AND ${f} MATCHES "^(pty_process_unix.c)$") list(APPEND to_remove ${sfile}) endif() + if(NOT WIN32 AND ${f} MATCHES "^(pty_process_win.c)$") + list(APPEND to_remove ${sfile}) + endif() endforeach() list(REMOVE_ITEM NVIM_SOURCES ${to_remove}) @@ -350,6 +353,10 @@ if(Iconv_LIBRARIES) list(APPEND NVIM_LINK_LIBRARIES ${Iconv_LIBRARIES}) endif() +if(WIN32) + list(APPEND NVIM_LINK_LIBRARIES ${WINPTY_LIBRARIES}) +endif() + # Put these last on the link line, since multiple things may depend on them. list(APPEND NVIM_LINK_LIBRARIES ${LIBUV_LIBRARIES} @@ -415,6 +422,7 @@ if(WIN32) COMMAND ${CMAKE_COMMAND} -E copy "${DEPS_PREFIX}/bin/tee.exe" ${PROJECT_BINARY_DIR}/windows_runtime_deps/ COMMAND ${CMAKE_COMMAND} -E copy "${DEPS_PREFIX}/bin/tidy.exe" ${PROJECT_BINARY_DIR}/windows_runtime_deps/ COMMAND ${CMAKE_COMMAND} -E copy "${DEPS_PREFIX}/bin/win32yank.exe" ${PROJECT_BINARY_DIR}/windows_runtime_deps/ + COMMAND ${CMAKE_COMMAND} -E copy "${DEPS_PREFIX}/bin/winpty-agent.exe" ${PROJECT_BINARY_DIR}/windows_runtime_deps/ COMMAND ${CMAKE_COMMAND} -E copy "${DEPS_PREFIX}/bin/D3Dcompiler_47.dll" ${PROJECT_BINARY_DIR}/windows_runtime_deps/ COMMAND ${CMAKE_COMMAND} -E copy "${DEPS_PREFIX}/bin/libEGL.dll" ${PROJECT_BINARY_DIR}/windows_runtime_deps/ @@ -428,6 +436,7 @@ if(WIN32) COMMAND ${CMAKE_COMMAND} -E copy "${DEPS_PREFIX}/bin/Qt5Network.dll" ${PROJECT_BINARY_DIR}/windows_runtime_deps/ COMMAND ${CMAKE_COMMAND} -E copy "${DEPS_PREFIX}/bin/Qt5Svg.dll" ${PROJECT_BINARY_DIR}/windows_runtime_deps/ COMMAND ${CMAKE_COMMAND} -E copy "${DEPS_PREFIX}/bin/Qt5Widgets.dll" ${PROJECT_BINARY_DIR}/windows_runtime_deps/ + COMMAND ${CMAKE_COMMAND} -E copy "${DEPS_PREFIX}/bin/winpty.dll" ${PROJECT_BINARY_DIR}/windows_runtime_deps/ COMMAND ${CMAKE_COMMAND} -E copy "${DEPS_PREFIX}/bin/platforms/qwindows.dll" ${PROJECT_BINARY_DIR}/windows_runtime_deps/platforms/ ) @@ -526,6 +535,7 @@ endfunction() set(NO_SINGLE_CHECK_HEADERS os/win_defs.h + os/pty_process_win.h regexp_defs.h syntax_defs.h terminal.h diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index 1ed2bc013e..e736e29e2d 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -37,7 +37,72 @@ typedef struct { # include "api/private/ui_events_metadata.generated.h" #endif +/// Start block that may cause VimL exceptions while evaluating another code +/// +/// Used when caller is supposed to be operating when other VimL code is being +/// processed and that “other VimL code” must not be affected. +/// +/// @param[out] tstate Location where try state should be saved. +void try_enter(TryState *const tstate) +{ + *tstate = (TryState) { + .current_exception = current_exception, + .msg_list = (const struct msglist *const *)msg_list, + .private_msg_list = NULL, + .trylevel = trylevel, + .got_int = got_int, + .did_throw = did_throw, + .need_rethrow = need_rethrow, + .did_emsg = did_emsg, + }; + msg_list = &tstate->private_msg_list; + current_exception = NULL; + trylevel = 1; + got_int = false; + did_throw = false; + need_rethrow = false; + did_emsg = false; +} + +/// End try block, set the error message if any and restore previous state +/// +/// @warning Return is consistent with most functions (false on error), not with +/// try_end (true on error). +/// +/// @param[in] tstate Previous state to restore. +/// @param[out] err Location where error should be saved. +/// +/// @return false if error occurred, true otherwise. +bool try_leave(const TryState *const tstate, Error *const err) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + const bool ret = !try_end(err); + assert(trylevel == 0); + assert(!need_rethrow); + assert(!got_int); + assert(!did_throw); + assert(!did_emsg); + assert(msg_list == &tstate->private_msg_list); + assert(*msg_list == NULL); + assert(current_exception == NULL); + msg_list = (struct msglist **)tstate->msg_list; + current_exception = tstate->current_exception; + trylevel = tstate->trylevel; + got_int = tstate->got_int; + did_throw = tstate->did_throw; + need_rethrow = tstate->need_rethrow; + did_emsg = tstate->did_emsg; + return ret; +} + /// Start block that may cause vimscript exceptions +/// +/// Each try_start() call should be mirrored by try_end() call. +/// +/// To be used as a replacement of `:try … catch … endtry` in C code, in cases +/// when error flag could not already be set. If there may be pending error +/// state at the time try_start() is executed which needs to be preserved, +/// try_enter()/try_leave() pair should be used instead. void try_start(void) { ++trylevel; @@ -50,7 +115,9 @@ void try_start(void) /// @return true if an error occurred bool try_end(Error *err) { - --trylevel; + // Note: all globals manipulated here should be saved/restored in + // try_enter/try_leave. + trylevel--; // Without this it stops processing all subsequent VimL commands and // generates strange error messages if I e.g. try calling Test() in a diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h index 159b9d5c2a..87f334ac30 100644 --- a/src/nvim/api/private/helpers.h +++ b/src/nvim/api/private/helpers.h @@ -6,6 +6,7 @@ #include "nvim/api/private/defs.h" #include "nvim/vim.h" #include "nvim/memory.h" +#include "nvim/ex_eval.h" #include "nvim/lib/kvec.h" #define OBJECT_OBJ(o) o @@ -82,6 +83,21 @@ #define api_free_window(value) #define api_free_tabpage(value) +/// Structure used for saving state for :try +/// +/// Used when caller is supposed to be operating when other VimL code is being +/// processed and that “other VimL code” must not be affected. +typedef struct { + except_T *current_exception; + struct msglist *private_msg_list; + const struct msglist *const *msg_list; + int trylevel; + int got_int; + int did_throw; + int need_rethrow; + int did_emsg; +} TryState; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/private/helpers.h.generated.h" #endif diff --git a/src/nvim/diff.c b/src/nvim/diff.c index 7da6665cb7..17de4284ce 100644 --- a/src/nvim/diff.c +++ b/src/nvim/diff.c @@ -1033,10 +1033,7 @@ void ex_diffsplit(exarg_T *eap) if (bufref_valid(&old_curbuf)) { // Move the cursor position to that of the old window. curwin->w_cursor.lnum = diff_get_corresponding_line( - old_curbuf.br_buf, - old_curwin->w_cursor.lnum, - curbuf, - curwin->w_cursor.lnum); + old_curbuf.br_buf, old_curwin->w_cursor.lnum); } } // Now that lines are folded scroll to show the cursor at the same @@ -2463,25 +2460,17 @@ int diff_move_to(int dir, long count) return OK; } -/// Finds the corresponding line in a diff. -/// -/// @param buf1 -/// @param lnum1 -/// @param buf2 -/// @param lnum3 -/// -/// @return The corresponding line. -linenr_T diff_get_corresponding_line(buf_T *buf1, linenr_T lnum1, buf_T *buf2, - linenr_T lnum3) +/// Return the line number in the current window that is closest to "lnum1" in +/// "buf1" in diff mode. +static linenr_T diff_get_corresponding_line_int(buf_T *buf1, linenr_T lnum1) { int idx1; int idx2; diff_T *dp; int baseline = 0; - linenr_T lnum2; idx1 = diff_buf_idx(buf1); - idx2 = diff_buf_idx(buf2); + idx2 = diff_buf_idx(curbuf); if ((idx1 == DB_COUNT) || (idx2 == DB_COUNT) @@ -2501,15 +2490,9 @@ linenr_T diff_get_corresponding_line(buf_T *buf1, linenr_T lnum1, buf_T *buf2, for (dp = curtab->tp_first_diff; dp != NULL; dp = dp->df_next) { if (dp->df_lnum[idx1] > lnum1) { - lnum2 = lnum1 - baseline; - - // don't end up past the end of the file - if (lnum2 > buf2->b_ml.ml_line_count) { - lnum2 = buf2->b_ml.ml_line_count; - } - - return lnum2; - } else if ((dp->df_lnum[idx1] + dp->df_count[idx1]) > lnum1) { + return lnum1 - baseline; + } + if ((dp->df_lnum[idx1] + dp->df_count[idx1]) > lnum1) { // Inside the diffblock baseline = lnum1 - dp->df_lnum[idx1]; @@ -2518,30 +2501,42 @@ linenr_T diff_get_corresponding_line(buf_T *buf1, linenr_T lnum1, buf_T *buf2, } return dp->df_lnum[idx2] + baseline; - } else if ((dp->df_lnum[idx1] == lnum1) - && (dp->df_count[idx1] == 0) - && (dp->df_lnum[idx2] <= lnum3) - && ((dp->df_lnum[idx2] + dp->df_count[idx2]) > lnum3)) { + } + if ((dp->df_lnum[idx1] == lnum1) + && (dp->df_count[idx1] == 0) + && (dp->df_lnum[idx2] <= curwin->w_cursor.lnum) + && ((dp->df_lnum[idx2] + dp->df_count[idx2]) + > curwin->w_cursor.lnum)) { // Special case: if the cursor is just after a zero-count // block (i.e. all filler) and the target cursor is already // inside the corresponding block, leave the target cursor // unmoved. This makes repeated CTRL-W W operations work // as expected. - return lnum3; + return curwin->w_cursor.lnum; } baseline = (dp->df_lnum[idx1] + dp->df_count[idx1]) - - (dp->df_lnum[idx2] + dp->df_count[idx2]); + - (dp->df_lnum[idx2] + dp->df_count[idx2]); } // If we get here then the cursor is after the last diff - lnum2 = lnum1 - baseline; + return lnum1 - baseline; +} + +/// Finds the corresponding line in a diff. +/// +/// @param buf1 +/// @param lnum1 +/// +/// @return The corresponding line. +linenr_T diff_get_corresponding_line(buf_T *buf1, linenr_T lnum1) +{ + linenr_T lnum = diff_get_corresponding_line_int(buf1, lnum1); // don't end up past the end of the file - if (lnum2 > buf2->b_ml.ml_line_count) { - lnum2 = buf2->b_ml.ml_line_count; + if (lnum > curbuf->b_ml.ml_line_count) { + return curbuf->b_ml.ml_line_count; } - - return lnum2; + return lnum; } /// For line "lnum" in the current window find the equivalent lnum in window diff --git a/src/nvim/eval.c b/src/nvim/eval.c index c42929ef7c..ac22d75a83 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -11031,6 +11031,7 @@ void get_user_input(const typval_T *const argvars, const char *defstr = ""; const char *cancelreturn = NULL; const char *xp_name = NULL; + Callback input_callback = { .type = kCallbackNone }; char prompt_buf[NUMBUFLEN]; char defstr_buf[NUMBUFLEN]; char cancelreturn_buf[NUMBUFLEN]; @@ -11040,7 +11041,7 @@ void get_user_input(const typval_T *const argvars, emsgf(_("E5050: {opts} must be the only argument")); return; } - const dict_T *const dict = argvars[0].vval.v_dict; + dict_T *const dict = argvars[0].vval.v_dict; prompt = tv_dict_get_string_buf_chk(dict, S_LEN("prompt"), prompt_buf, ""); if (prompt == NULL) { return; @@ -11066,6 +11067,9 @@ void get_user_input(const typval_T *const argvars, if (xp_name == def) { // default to NULL xp_name = NULL; } + if (!tv_dict_get_callback(dict, S_LEN("highlight"), &input_callback)) { + return; + } } else { prompt = tv_get_string_buf_chk(&argvars[0], prompt_buf); if (prompt == NULL) { @@ -11124,12 +11128,13 @@ void get_user_input(const typval_T *const argvars, stuffReadbuffSpec(defstr); - int save_ex_normal_busy = ex_normal_busy; + const int save_ex_normal_busy = ex_normal_busy; ex_normal_busy = 0; rettv->vval.v_string = - getcmdline_prompt(inputsecret_flag ? NUL : '@', (char_u *)p, echo_attr, - xp_type, (char_u *)xp_arg); + (char_u *)getcmdline_prompt(inputsecret_flag ? NUL : '@', p, echo_attr, + xp_type, xp_arg, input_callback); ex_normal_busy = save_ex_normal_busy; + callback_free(&input_callback); if (rettv->vval.v_string == NULL && cancelreturn != NULL) { rettv->vval.v_string = (char_u *)xstrdup(cancelreturn); diff --git a/src/nvim/eval/typval.h b/src/nvim/eval/typval.h index 3f8ed3b3f9..c44b85644d 100644 --- a/src/nvim/eval/typval.h +++ b/src/nvim/eval/typval.h @@ -43,7 +43,7 @@ typedef struct partial_S partial_T; typedef struct ufunc ufunc_T; typedef enum { - kCallbackNone, + kCallbackNone = 0, kCallbackFuncref, kCallbackPartial, } CallbackType; diff --git a/src/nvim/ex_cmds2.c b/src/nvim/ex_cmds2.c index 1a728647ca..33fe30cd5a 100644 --- a/src/nvim/ex_cmds2.c +++ b/src/nvim/ex_cmds2.c @@ -189,7 +189,8 @@ void do_debug(char_u *cmd) } xfree(cmdline); - cmdline = getcmdline_prompt('>', NULL, 0, EXPAND_NOTHING, NULL); + cmdline = (char_u *)getcmdline_prompt('>', NULL, 0, EXPAND_NOTHING, NULL, + CALLBACK_NONE); if (typeahead_saved) { restore_typeahead(&typeaheadbuf); @@ -3610,18 +3611,11 @@ void ex_language(exarg_T *eap) static char_u **locales = NULL; // Array of all available locales -static bool did_init_locales = false; -/// Lazy initialization of all available locales. -static void init_locales(void) -{ - if (!did_init_locales) { - did_init_locales = true; - locales = find_locales(); - } -} +#ifndef WIN32 +static bool did_init_locales = false; -// Return an array of strings for all available locales + NULL for the +/// Return an array of strings for all available locales + NULL for the /// last element. Return NULL in case of error. static char_u **find_locales(void) { @@ -3653,6 +3647,18 @@ static char_u **find_locales(void) ((char_u **)locales_ga.ga_data)[locales_ga.ga_len] = NULL; return (char_u **)locales_ga.ga_data; } +#endif + +/// Lazy initialization of all available locales. +static void init_locales(void) +{ +#ifndef WIN32 + if (!did_init_locales) { + did_init_locales = true; + locales = find_locales(); + } +#endif +} # if defined(EXITFREE) void free_locales(void) diff --git a/src/nvim/ex_eval.c b/src/nvim/ex_eval.c index c029df2f13..9037b3c151 100644 --- a/src/nvim/ex_eval.c +++ b/src/nvim/ex_eval.c @@ -561,7 +561,9 @@ static void discard_exception(except_T *excp, int was_finished) */ void discard_current_exception(void) { - discard_exception(current_exception, FALSE); + discard_exception(current_exception, false); + // Note: all globals manipulated here should be saved/restored in + // try_enter/try_leave. current_exception = NULL; did_throw = FALSE; need_rethrow = FALSE; diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index ecd5c81822..5e216925df 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -63,6 +63,9 @@ #include "nvim/os/os.h" #include "nvim/event/loop.h" #include "nvim/os/time.h" +#include "nvim/lib/kvec.h" +#include "nvim/api/private/helpers.h" +#include "nvim/highlight_defs.h" /* * Variables shared between getcmdline(), redrawcmdline() and others. @@ -70,23 +73,27 @@ * structure. */ struct cmdline_info { - char_u *cmdbuff; /* pointer to command line buffer */ - int cmdbufflen; /* length of cmdbuff */ - int cmdlen; /* number of chars in command line */ - int cmdpos; /* current cursor position */ - int cmdspos; /* cursor column on screen */ - int cmdfirstc; /* ':', '/', '?', '=', '>' or NUL */ - int cmdindent; /* number of spaces before cmdline */ - char_u *cmdprompt; /* message in front of cmdline */ - int cmdattr; /* attributes for prompt */ - int overstrike; /* Typing mode on the command line. Shared by - getcmdline() and put_on_cmdline(). */ - expand_T *xpc; /* struct being used for expansion, xp_pattern - may point into cmdbuff */ - int xp_context; /* type of expansion */ - char_u *xp_arg; /* user-defined expansion arg */ - int input_fn; /* when TRUE Invoked for input() function */ + char_u *cmdbuff; // pointer to command line buffer + int cmdbufflen; // length of cmdbuff + int cmdlen; // number of chars in command line + int cmdpos; // current cursor position + int cmdspos; // cursor column on screen + int cmdfirstc; // ':', '/', '?', '=', '>' or NUL + int cmdindent; // number of spaces before cmdline + char_u *cmdprompt; // message in front of cmdline + int cmdattr; // attributes for prompt + int overstrike; // Typing mode on the command line. Shared by + // getcmdline() and put_on_cmdline(). + expand_T *xpc; // struct being used for expansion, xp_pattern + // may point into cmdbuff + int xp_context; // type of expansion + char_u *xp_arg; // user-defined expansion arg + int input_fn; // when TRUE Invoked for input() function + unsigned prompt_id; ///< Prompt number, used to disable coloring on errors. + Callback highlight_callback; ///< Callback used for coloring user input. }; +/// Last value of prompt_id, incremented when doing new prompt +static unsigned last_prompt_id = 0; typedef struct command_line_state { VimState state; @@ -136,6 +143,38 @@ typedef struct command_line_state { struct cmdline_info save_ccline; } CommandLineState; +/// Command-line colors: one chunk +/// +/// Defines a region which has the same highlighting. +typedef struct { + int start; ///< Colored chunk start. + int end; ///< Colored chunk end (exclusive, > start). + int attr; ///< Highlight attr. +} CmdlineColorChunk; + +/// Command-line colors +/// +/// Holds data about all colors. +typedef kvec_t(CmdlineColorChunk) CmdlineColors; + +/// Command-line coloring +/// +/// Holds both what are the colors and what have been colored. Latter is used to +/// suppress unnecessary calls to coloring callbacks. +typedef struct { + unsigned prompt_id; ///< ID of the prompt which was colored last. + char *cmdbuff; ///< What exactly was colored last time or NULL. + CmdlineColors colors; ///< Last colors. +} ColoredCmdline; + +/// Last command-line colors. +ColoredCmdline last_ccline_colors = { + .cmdbuff = NULL, + .colors = KV_INITIAL_VALUE +}; + +typedef struct cmdline_info CmdlineInfo; + /* The current cmdline_info. It is initialized in getcmdline() and after that * used by other functions. When invoking getcmdline() recursively it needs * to be saved with save_cmdline() and restored with restore_cmdline(). @@ -157,6 +196,12 @@ static int hisnum[HIST_COUNT] = {0, 0, 0, 0, 0}; /* identifying (unique) number of newest history entry */ static int hislen = 0; /* actual length of history tables */ +/// Flag for command_line_handle_key to ignore <C-c> +/// +/// Used if it was received while processing highlight function in order for +/// user interrupting highlight function to not interrupt command-line. +static bool getln_interrupted_highlight = false; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "ex_getln.c.generated.h" @@ -193,6 +238,7 @@ static uint8_t *command_line_enter(int firstc, long count, int indent) cmd_hkmap = 0; } + ccline.prompt_id = last_prompt_id++; ccline.overstrike = false; // always start in insert mode clearpos(&s->match_end); s->save_cursor = curwin->w_cursor; // may be restored later @@ -1160,8 +1206,11 @@ static int command_line_handle_key(CommandLineState *s) case ESC: // get here if p_wc != ESC or when ESC typed twice case Ctrl_C: // In exmode it doesn't make sense to return. Except when - // ":normal" runs out of characters. - if (exmode_active && (ex_normal_busy == 0 || typebuf.tb_len > 0)) { + // ":normal" runs out of characters. Also when highlight callback is active + // <C-c> should interrupt only it. + if ((exmode_active && (ex_normal_busy == 0 || typebuf.tb_len > 0)) + || (getln_interrupted_highlight && s->c == Ctrl_C)) { + getln_interrupted_highlight = false; return command_line_not_changed(s); } @@ -1790,41 +1839,50 @@ getcmdline ( return command_line_enter(firstc, count, indent); } -/* - * Get a command line with a prompt. - * This is prepared to be called recursively from getcmdline() (e.g. by - * f_input() when evaluating an expression from CTRL-R =). - * Returns the command line in allocated memory, or NULL. - */ -char_u * -getcmdline_prompt ( - int firstc, - char_u *prompt, /* command line prompt */ - int attr, /* attributes for prompt */ - int xp_context, /* type of expansion */ - char_u *xp_arg /* user-defined expansion argument */ -) +/// Get a command line with a prompt +/// +/// This is prepared to be called recursively from getcmdline() (e.g. by +/// f_input() when evaluating an expression from `<C-r>=`). +/// +/// @param[in] firstc Prompt type: e.g. '@' for input(), '>' for debug. +/// @param[in] prompt Prompt string: what is displayed before the user text. +/// @param[in] attr Prompt highlighting. +/// @param[in] xp_context Type of expansion. +/// @param[in] xp_arg User-defined expansion argument. +/// @param[in] highlight_callback Callback used for highlighting user input. +/// +/// @return [allocated] Command line or NULL. +char *getcmdline_prompt(const char firstc, const char *const prompt, + const int attr, const int xp_context, + const char *const xp_arg, + const Callback highlight_callback) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_MALLOC { - char_u *s; - struct cmdline_info save_ccline; - int msg_col_save = msg_col; + const int msg_col_save = msg_col; + struct cmdline_info save_ccline; save_cmdline(&save_ccline); - ccline.cmdprompt = prompt; + + ccline.prompt_id = last_prompt_id++; + ccline.cmdprompt = (char_u *)prompt; ccline.cmdattr = attr; ccline.xp_context = xp_context; - ccline.xp_arg = xp_arg; + ccline.xp_arg = (char_u *)xp_arg; ccline.input_fn = (firstc == '@'); - s = getcmdline(firstc, 1L, 0); + ccline.highlight_callback = highlight_callback; + + char *const ret = (char *)getcmdline(firstc, 1L, 0); + restore_cmdline(&save_ccline); - /* Restore msg_col, the prompt from input() may have changed it. - * But only if called recursively and the commandline is therefore being - * restored to an old one; if not, the input() prompt stays on the screen, - * so we need its modified msg_col left intact. */ - if (ccline.cmdbuff != NULL) + // Restore msg_col, the prompt from input() may have changed it. + // But only if called recursively and the commandline is therefore being + // restored to an old one; if not, the input() prompt stays on the screen, + // so we need its modified msg_col left intact. + if (ccline.cmdbuff != NULL) { msg_col = msg_col_save; + } - return s; + return ret; } /* @@ -2285,75 +2343,329 @@ void free_cmdline_buf(void) # endif +enum { MAX_CB_ERRORS = 1 }; + +/// Color command-line +/// +/// Should use built-in command parser or user-specified one. Currently only the +/// latter is supported. +/// +/// @param[in] colored_ccline Command-line to color. +/// @param[out] ret_ccline_colors What should be colored. Also holds a cache: +/// if ->prompt_id and ->cmdbuff values happen +/// to be equal to those from colored_cmdline it +/// will just do nothing, assuming that ->colors +/// already contains needed data. +/// +/// Always colors the whole cmdline. +/// +/// @return true if draw_cmdline may proceed, false if it does not need anything +/// to do. +static bool color_cmdline(const CmdlineInfo *const colored_ccline, + ColoredCmdline *const ret_ccline_colors) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + bool printed_errmsg = false; +#define PRINT_ERRMSG(...) \ + do { \ + msg_putchar('\n'); \ + msg_printf_attr(hl_attr(HLF_E)|MSG_HIST, __VA_ARGS__); \ + printed_errmsg = true; \ + } while (0) + bool ret = true; + + // Check whether result of the previous call is still valid. + if (ret_ccline_colors->prompt_id == colored_ccline->prompt_id + && ret_ccline_colors->cmdbuff != NULL + && STRCMP(ret_ccline_colors->cmdbuff, colored_ccline->cmdbuff) == 0) { + return ret; + } + + kv_size(ret_ccline_colors->colors) = 0; + + if (colored_ccline->cmdbuff == NULL || *colored_ccline->cmdbuff == NUL) { + // Nothing to do, exiting. + xfree(ret_ccline_colors->cmdbuff); + ret_ccline_colors->cmdbuff = NULL; + return ret; + } + + bool arg_allocated = false; + typval_T arg = { + .v_type = VAR_STRING, + .vval.v_string = colored_ccline->cmdbuff, + }; + typval_T tv = { .v_type = VAR_UNKNOWN }; + + static unsigned prev_prompt_id = UINT_MAX; + static int prev_prompt_errors = 0; + Callback color_cb = { .type = kCallbackNone }; + bool can_free_cb = false; + TryState tstate; + Error err = ERROR_INIT; + const char *err_errmsg = (const char *)e_intern2; + bool dgc_ret = true; + bool tl_ret = true; + + if (colored_ccline->prompt_id != prev_prompt_id) { + prev_prompt_errors = 0; + prev_prompt_id = colored_ccline->prompt_id; + } else if (prev_prompt_errors >= MAX_CB_ERRORS) { + goto color_cmdline_end; + } + if (colored_ccline->highlight_callback.type != kCallbackNone) { + // Currently this should only happen while processing input() prompts. + assert(colored_ccline->input_fn); + color_cb = colored_ccline->highlight_callback; + } else if (colored_ccline->cmdfirstc == ':') { + try_enter(&tstate); + err_errmsg = N_( + "E5408: Unable to get g:Nvim_color_cmdline callback: %s"); + dgc_ret = tv_dict_get_callback(&globvardict, S_LEN("Nvim_color_cmdline"), + &color_cb); + tl_ret = try_leave(&tstate, &err); + can_free_cb = true; + } else if (colored_ccline->cmdfirstc == '=') { + try_enter(&tstate); + err_errmsg = N_( + "E5409: Unable to get g:Nvim_color_expr callback: %s"); + dgc_ret = tv_dict_get_callback(&globvardict, S_LEN("Nvim_color_expr"), + &color_cb); + tl_ret = try_leave(&tstate, &err); + can_free_cb = true; + } + if (!tl_ret || !dgc_ret) { + goto color_cmdline_error; + } + + if (color_cb.type == kCallbackNone) { + goto color_cmdline_end; + } + if (colored_ccline->cmdbuff[colored_ccline->cmdlen] != NUL) { + arg_allocated = true; + arg.vval.v_string = xmemdupz((const char *)colored_ccline->cmdbuff, + (size_t)colored_ccline->cmdlen); + } + // msg_start() called by e.g. :echo may shift command-line to the first column + // even though msg_silent is here. Two ways to workaround this problem without + // altering message.c: use full_screen or save and restore msg_col. + // + // Saving and restoring full_screen does not work well with :redraw!. Saving + // and restoring msg_col is neither ideal, but while with full_screen it + // appears shifted one character to the right and cursor position is no longer + // correct, with msg_col it just misses leading `:`. Since `redraw!` in + // callback lags this is least of the user problems. + // + // Also using try_enter() because error messages may overwrite typed + // command-line which is not expected. + getln_interrupted_highlight = false; + try_enter(&tstate); + err_errmsg = N_("E5407: Callback has thrown an exception: %s"); + const int saved_msg_col = msg_col; + msg_silent++; + const bool cbcall_ret = callback_call(&color_cb, 1, &arg, &tv); + msg_silent--; + msg_col = saved_msg_col; + if (got_int) { + getln_interrupted_highlight = true; + } + if (!try_leave(&tstate, &err) || !cbcall_ret) { + goto color_cmdline_error; + } + if (tv.v_type != VAR_LIST) { + PRINT_ERRMSG(_("E5400: Callback should return list")); + goto color_cmdline_error; + } + if (tv.vval.v_list == NULL) { + goto color_cmdline_end; + } + varnumber_T prev_end = 0; + int i = 0; + for (const listitem_T *li = tv.vval.v_list->lv_first; + li != NULL; li = li->li_next, i++) { + if (li->li_tv.v_type != VAR_LIST) { + PRINT_ERRMSG(_("E5401: List item %i is not a List"), i); + goto color_cmdline_error; + } + const list_T *const l = li->li_tv.vval.v_list; + if (tv_list_len(l) != 3) { + PRINT_ERRMSG(_("E5402: List item %i has incorrect length: %li /= 3"), + i, tv_list_len(l)); + goto color_cmdline_error; + } + bool error = false; + const varnumber_T start = tv_get_number_chk(&l->lv_first->li_tv, &error); + if (error) { + goto color_cmdline_error; + } else if (!(prev_end <= start && start < colored_ccline->cmdlen)) { + PRINT_ERRMSG(_("E5403: Chunk %i start %" PRIdVARNUMBER " not in range " + "[%" PRIdVARNUMBER ", %i)"), + i, start, prev_end, colored_ccline->cmdlen); + goto color_cmdline_error; + } else if (utf8len_tab_zero[(uint8_t)colored_ccline->cmdbuff[start]] == 0) { + PRINT_ERRMSG(_("E5405: Chunk %i start %" PRIdVARNUMBER " splits " + "multibyte character"), i, start); + goto color_cmdline_error; + } + if (start != prev_end) { + kv_push(ret_ccline_colors->colors, ((CmdlineColorChunk) { + .start = prev_end, + .end = start, + .attr = 0, + })); + } + const varnumber_T end = tv_get_number_chk(&l->lv_first->li_next->li_tv, + &error); + if (error) { + goto color_cmdline_error; + } else if (!(start < end && end <= colored_ccline->cmdlen)) { + PRINT_ERRMSG(_("E5404: Chunk %i end %" PRIdVARNUMBER " not in range " + "(%" PRIdVARNUMBER ", %i]"), + i, end, start, colored_ccline->cmdlen); + goto color_cmdline_error; + } else if (end < colored_ccline->cmdlen + && (utf8len_tab_zero[(uint8_t)colored_ccline->cmdbuff[end]] + == 0)) { + PRINT_ERRMSG(_("E5406: Chunk %i end %" PRIdVARNUMBER " splits multibyte " + "character"), i, end); + goto color_cmdline_error; + } + prev_end = end; + const char *const group = tv_get_string_chk(&l->lv_last->li_tv); + if (group == NULL) { + goto color_cmdline_error; + } + const int id = syn_name2id((char_u *)group); + const int attr = (id == 0 ? 0 : syn_id2attr(id)); + kv_push(ret_ccline_colors->colors, ((CmdlineColorChunk) { + .start = start, + .end = end, + .attr = attr, + })); + } + if (prev_end < colored_ccline->cmdlen) { + kv_push(ret_ccline_colors->colors, ((CmdlineColorChunk) { + .start = prev_end, + .end = colored_ccline->cmdlen, + .attr = 0, + })); + } + prev_prompt_errors = 0; +color_cmdline_end: + assert(!ERROR_SET(&err)); + if (can_free_cb) { + callback_free(&color_cb); + } + xfree(ret_ccline_colors->cmdbuff); + // Note: errors “output” is cached just as well as regular results. + ret_ccline_colors->prompt_id = colored_ccline->prompt_id; + if (arg_allocated) { + ret_ccline_colors->cmdbuff = (char *)arg.vval.v_string; + } else { + ret_ccline_colors->cmdbuff = xmemdupz((const char *)colored_ccline->cmdbuff, + (size_t)colored_ccline->cmdlen); + } + tv_clear(&tv); + return ret; +color_cmdline_error: + if (ERROR_SET(&err)) { + PRINT_ERRMSG(_(err_errmsg), err.msg); + api_clear_error(&err); + } + assert(printed_errmsg); + (void)printed_errmsg; + + prev_prompt_errors++; + kv_size(ret_ccline_colors->colors) = 0; + redrawcmdline(); + ret = false; + goto color_cmdline_end; +#undef PRINT_ERRMSG +} + /* * Draw part of the cmdline at the current cursor position. But draw stars * when cmdline_star is TRUE. */ static void draw_cmdline(int start, int len) { - int i; + if (!color_cmdline(&ccline, &last_ccline_colors)) { + return; + } - if (cmdline_star > 0) - for (i = 0; i < len; ++i) { + if (cmdline_star > 0) { + for (int i = 0; i < len; i++) { msg_putchar('*'); - if (has_mbyte) + if (has_mbyte) { i += (*mb_ptr2len)(ccline.cmdbuff + start + i) - 1; + } } - else if (p_arshape && !p_tbidi && enc_utf8 && len > 0) { - static int buflen = 0; - char_u *p; - int j; - int newlen = 0; + } else if (p_arshape && !p_tbidi && enc_utf8 && len > 0) { + bool do_arabicshape = false; int mb_l; - int pc, pc1 = 0; - int prev_c = 0; - int prev_c1 = 0; - int u8c; - int u8cc[MAX_MCO]; - int nc = 0; + for (int i = start; i < start + len; i += mb_l) { + char_u *p = ccline.cmdbuff + i; + int u8cc[MAX_MCO]; + int u8c = utfc_ptr2char_len(p, u8cc, start + len - i); + mb_l = utfc_ptr2len_len(p, start + len - i); + if (arabic_char(u8c)) { + do_arabicshape = true; + break; + } + } + if (!do_arabicshape) { + goto draw_cmdline_no_arabicshape; + } - /* - * Do arabic shaping into a temporary buffer. This is very - * inefficient! - */ + static int buflen = 0; + + // Do arabic shaping into a temporary buffer. This is very + // inefficient! if (len * 2 + 2 > buflen) { - /* Re-allocate the buffer. We keep it around to avoid a lot of - * alloc()/free() calls. */ + // Re-allocate the buffer. We keep it around to avoid a lot of + // alloc()/free() calls. xfree(arshape_buf); buflen = len * 2 + 2; arshape_buf = xmalloc(buflen); } + int newlen = 0; if (utf_iscomposing(utf_ptr2char(ccline.cmdbuff + start))) { - /* Prepend a space to draw the leading composing char on. */ + // Prepend a space to draw the leading composing char on. arshape_buf[0] = ' '; newlen = 1; } - for (j = start; j < start + len; j += mb_l) { - p = ccline.cmdbuff + j; - u8c = utfc_ptr2char_len(p, u8cc, start + len - j); - mb_l = utfc_ptr2len_len(p, start + len - j); + int prev_c = 0; + int prev_c1 = 0; + for (int i = start; i < start + len; i += mb_l) { + char_u *p = ccline.cmdbuff + i; + int u8cc[MAX_MCO]; + int u8c = utfc_ptr2char_len(p, u8cc, start + len - i); + mb_l = utfc_ptr2len_len(p, start + len - i); if (arabic_char(u8c)) { - /* Do Arabic shaping. */ + int pc; + int pc1 = 0; + int nc = 0; + // Do Arabic shaping. if (cmdmsg_rl) { - /* displaying from right to left */ + // Displaying from right to left. pc = prev_c; pc1 = prev_c1; prev_c1 = u8cc[0]; - if (j + mb_l >= start + len) + if (i + mb_l >= start + len) { nc = NUL; - else + } else { nc = utf_ptr2char(p + mb_l); + } } else { - /* displaying from left to right */ - if (j + mb_l >= start + len) + // Displaying from left to right. + if (i + mb_l >= start + len) { pc = NUL; - else { + } else { int pcc[MAX_MCO]; - pc = utfc_ptr2char_len(p + mb_l, pcc, - start + len - j - mb_l); + pc = utfc_ptr2char_len(p + mb_l, pcc, start + len - i - mb_l); pc1 = pcc[0]; } nc = prev_c; @@ -2377,8 +2689,23 @@ static void draw_cmdline(int start, int len) } msg_outtrans_len(arshape_buf, newlen); - } else - msg_outtrans_len(ccline.cmdbuff + start, len); + } else { +draw_cmdline_no_arabicshape: + if (kv_size(last_ccline_colors.colors)) { + for (size_t i = 0; i < kv_size(last_ccline_colors.colors); i++) { + CmdlineColorChunk chunk = kv_A(last_ccline_colors.colors, i); + if (chunk.end <= start) { + continue; + } + const int chunk_start = MAX(chunk.start, start); + msg_outtrans_len_attr(ccline.cmdbuff + chunk_start, + chunk.end - chunk_start, + chunk.attr); + } + } else { + msg_outtrans_len(ccline.cmdbuff + start, len); + } + } } /* diff --git a/src/nvim/mbyte.c b/src/nvim/mbyte.c index 3fad6c789d..b24770a409 100644 --- a/src/nvim/mbyte.c +++ b/src/nvim/mbyte.c @@ -75,7 +75,7 @@ struct interval { /* * Like utf8len_tab above, but using a zero for illegal lead bytes. */ -static uint8_t utf8len_tab_zero[256] = +const uint8_t utf8len_tab_zero[256] = { 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, diff --git a/src/nvim/mbyte.h b/src/nvim/mbyte.h index ad9e38004c..bf6ccb13a5 100644 --- a/src/nvim/mbyte.h +++ b/src/nvim/mbyte.h @@ -1,6 +1,7 @@ #ifndef NVIM_MBYTE_H #define NVIM_MBYTE_H +#include <stdint.h> #include <stdbool.h> #include <string.h> @@ -18,6 +19,9 @@ #define MB_BYTE2LEN(b) utf8len_tab[b] #define MB_BYTE2LEN_CHECK(b) (((b) < 0 || (b) > 255) ? 1 : utf8len_tab[b]) +// max length of an unicode char +#define MB_MAXCHAR 6 + /* properties used in enc_canon_table[] (first three mutually exclusive) */ #define ENC_8BIT 0x01 #define ENC_DBCS 0x02 @@ -67,6 +71,8 @@ typedef struct { ///< otherwise use '?'. } vimconv_T; +extern const uint8_t utf8len_tab_zero[256]; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "mbyte.h.generated.h" #endif diff --git a/src/nvim/menu.c b/src/nvim/menu.c index a498916e5e..0db250d111 100644 --- a/src/nvim/menu.c +++ b/src/nvim/menu.c @@ -666,8 +666,6 @@ static void free_menu_string(vimmenu_T *menu, int idx) static dict_T *menu_get_recursive(const vimmenu_T *menu, int modes) { dict_T *dict; - char buf[sizeof(menu->mnemonic)]; - int mnemonic_len; if (!menu || (menu->modes & modes) == 0x0) { return NULL; @@ -679,8 +677,8 @@ static dict_T *menu_get_recursive(const vimmenu_T *menu, int modes) tv_dict_add_nr(dict, S_LEN("hidden"), menu_is_hidden(menu->dname)); if (menu->mnemonic) { - mnemonic_len = utf_char2bytes(menu->mnemonic, (u_char *)buf); - buf[mnemonic_len] = '\0'; + char buf[MB_MAXCHAR + 1] = { 0 }; // > max value of utf8_char2bytes + utf_char2bytes(menu->mnemonic, (char_u *)buf); tv_dict_add_str(dict, S_LEN("shortcut"), buf); } @@ -717,7 +715,7 @@ static dict_T *menu_get_recursive(const vimmenu_T *menu, int modes) list_T *children_list = tv_list_alloc(); for (menu = menu->children; menu != NULL; menu = menu->next) { dict_T *dic = menu_get_recursive(menu, modes); - if (dict && tv_dict_len(dict) > 0) { + if (tv_dict_len(dict) > 0) { tv_list_append_dict(children_list, dic); } } diff --git a/src/nvim/message.c b/src/nvim/message.c index 28c88f5a14..b90c475ede 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -1628,6 +1628,27 @@ void msg_puts_attr_len(const char *const str, const ptrdiff_t len, int attr) } } +/// Print a formatted message +/// +/// Message printed is limited by #IOSIZE. Must not be used from inside +/// msg_puts_attr(). +/// +/// @param[in] attr Highlight attributes. +/// @param[in] fmt Format string. +void msg_printf_attr(const int attr, const char *const fmt, ...) + FUNC_ATTR_NONNULL_ARG(2) +{ + static char msgbuf[IOSIZE]; + + va_list ap; + va_start(ap, fmt); + const size_t len = vim_vsnprintf(msgbuf, sizeof(msgbuf), fmt, ap, NULL); + va_end(ap); + + msg_scroll = true; + msg_puts_attr_len(msgbuf, (ptrdiff_t)len, attr); +} + /* * The display part of msg_puts_attr_len(). * May be called recursively to display scroll-back text. diff --git a/src/nvim/move.c b/src/nvim/move.c index 81d46a7f17..4d646f5a4b 100644 --- a/src/nvim/move.c +++ b/src/nvim/move.c @@ -2147,14 +2147,12 @@ void do_check_cursorbind(void) curbuf = curwin->w_buffer; /* skip original window and windows with 'noscrollbind' */ if (curwin != old_curwin && curwin->w_p_crb) { - if (curwin->w_p_diff) - curwin->w_cursor.lnum - = diff_get_corresponding_line(old_curbuf, - line, - curbuf, - curwin->w_cursor.lnum); - else + if (curwin->w_p_diff) { + curwin->w_cursor.lnum = + diff_get_corresponding_line(old_curbuf, line); + } else { curwin->w_cursor.lnum = line; + } curwin->w_cursor.col = col; curwin->w_cursor.coladd = coladd; curwin->w_curswant = curswant; diff --git a/src/nvim/os/pty_process_win.c b/src/nvim/os/pty_process_win.c new file mode 100644 index 0000000000..ef8a699c56 --- /dev/null +++ b/src/nvim/os/pty_process_win.c @@ -0,0 +1,410 @@ +#include <assert.h> +#include <stdbool.h> +#include <stdlib.h> + +#include <winpty_constants.h> + +#include "nvim/os/os.h" +#include "nvim/ascii.h" +#include "nvim/memory.h" +#include "nvim/mbyte.h" // for utf8_to_utf16, utf16_to_utf8 +#include "nvim/os/pty_process_win.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "os/pty_process_win.c.generated.h" +#endif + +static void CALLBACK pty_process_finish1(void *context, BOOLEAN unused) + FUNC_ATTR_NONNULL_ALL +{ + PtyProcess *ptyproc = (PtyProcess *)context; + Process *proc = (Process *)ptyproc; + + uv_timer_init(&proc->loop->uv, &ptyproc->wait_eof_timer); + ptyproc->wait_eof_timer.data = (void *)ptyproc; + uv_timer_start(&ptyproc->wait_eof_timer, wait_eof_timer_cb, 200, 200); +} + +/// @returns zero on success, or negative error code. +int pty_process_spawn(PtyProcess *ptyproc) + FUNC_ATTR_NONNULL_ALL +{ + Process *proc = (Process *)ptyproc; + int status = 0; + winpty_error_ptr_t err = NULL; + winpty_config_t *cfg = NULL; + winpty_spawn_config_t *spawncfg = NULL; + winpty_t *winpty_object = NULL; + char *in_name = NULL; + char *out_name = NULL; + HANDLE process_handle = NULL; + uv_connect_t *in_req = NULL; + uv_connect_t *out_req = NULL; + wchar_t *cmd_line = NULL; + wchar_t *cwd = NULL; + const char *emsg = NULL; + + assert(!proc->err); + + cfg = winpty_config_new(WINPTY_FLAG_ALLOW_CURPROC_DESKTOP_CREATION, &err); + if (cfg == NULL) { + emsg = "Failed, winpty_config_new."; + goto cleanup; + } + + winpty_config_set_initial_size(cfg, ptyproc->width, ptyproc->height); + winpty_object = winpty_open(cfg, &err); + if (winpty_object == NULL) { + emsg = "Failed, winpty_open."; + goto cleanup; + } + + status = utf16_to_utf8(winpty_conin_name(winpty_object), &in_name); + if (status != 0) { + emsg = "Failed to convert in_name from utf16 to utf8."; + goto cleanup; + } + + status = utf16_to_utf8(winpty_conout_name(winpty_object), &out_name); + if (status != 0) { + emsg = "Failed to convert out_name from utf16 to utf8."; + goto cleanup; + } + + if (proc->in != NULL) { + in_req = xmalloc(sizeof(uv_connect_t)); + uv_pipe_connect( + in_req, + &proc->in->uv.pipe, + in_name, + pty_process_connect_cb); + } + + if (proc->out != NULL) { + out_req = xmalloc(sizeof(uv_connect_t)); + uv_pipe_connect( + out_req, + &proc->out->uv.pipe, + out_name, + pty_process_connect_cb); + } + + if (proc->cwd != NULL) { + status = utf8_to_utf16(proc->cwd, &cwd); + if (status != 0) { + emsg = "Failed to convert pwd form utf8 to utf16."; + goto cleanup; + } + } + + status = build_cmd_line(proc->argv, &cmd_line, + os_shell_is_cmdexe(proc->argv[0])); + if (status != 0) { + emsg = "Failed to convert cmd line form utf8 to utf16."; + goto cleanup; + } + + spawncfg = winpty_spawn_config_new( + WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN, + NULL, // Optional application name + cmd_line, + cwd, + NULL, // Optional environment variables + &err); + if (spawncfg == NULL) { + emsg = "Failed winpty_spawn_config_new."; + goto cleanup; + } + + DWORD win_err = 0; + if (!winpty_spawn(winpty_object, + spawncfg, + &process_handle, + NULL, // Optional thread handle + &win_err, + &err)) { + if (win_err) { + status = (int)win_err; + emsg = "Failed spawn process."; + } else { + emsg = "Failed winpty_spawn."; + } + goto cleanup; + } + proc->pid = GetProcessId(process_handle); + + if (!RegisterWaitForSingleObject( + &ptyproc->finish_wait, + process_handle, + pty_process_finish1, + ptyproc, + INFINITE, + WT_EXECUTEDEFAULT | WT_EXECUTEONLYONCE)) { + abort(); + } + + // Wait until pty_process_connect_cb is called. + while ((in_req != NULL && in_req->handle != NULL) + || (out_req != NULL && out_req->handle != NULL)) { + uv_run(&proc->loop->uv, UV_RUN_ONCE); + } + + ptyproc->winpty_object = winpty_object; + ptyproc->process_handle = process_handle; + winpty_object = NULL; + process_handle = NULL; + +cleanup: + if (status) { + // In the case of an error of MultiByteToWideChar or CreateProcessW. + ELOG("%s error code: %d", emsg, status); + status = os_translate_sys_error(status); + } else if (err != NULL) { + status = (int)winpty_error_code(err); + ELOG("%s error code: %d", emsg, status); + status = translate_winpty_error(status); + } + winpty_error_free(err); + winpty_config_free(cfg); + winpty_spawn_config_free(spawncfg); + winpty_free(winpty_object); + xfree(in_name); + xfree(out_name); + if (process_handle != NULL) { + CloseHandle(process_handle); + } + xfree(in_req); + xfree(out_req); + xfree(cmd_line); + xfree(cwd); + return status; +} + +void pty_process_resize(PtyProcess *ptyproc, uint16_t width, + uint16_t height) + FUNC_ATTR_NONNULL_ALL +{ + if (ptyproc->winpty_object != NULL) { + winpty_set_size(ptyproc->winpty_object, width, height, NULL); + } +} + +void pty_process_close(PtyProcess *ptyproc) + FUNC_ATTR_NONNULL_ALL +{ + Process *proc = (Process *)ptyproc; + + pty_process_close_master(ptyproc); + + if (proc->internal_close_cb) { + proc->internal_close_cb(proc); + } +} + +void pty_process_close_master(PtyProcess *ptyproc) + FUNC_ATTR_NONNULL_ALL +{ + if (ptyproc->winpty_object != NULL) { + winpty_free(ptyproc->winpty_object); + ptyproc->winpty_object = NULL; + } +} + +void pty_process_teardown(Loop *loop) + FUNC_ATTR_NONNULL_ALL +{ +} + +static void pty_process_connect_cb(uv_connect_t *req, int status) + FUNC_ATTR_NONNULL_ALL +{ + assert(status == 0); + req->handle = NULL; +} + +static void wait_eof_timer_cb(uv_timer_t *wait_eof_timer) + FUNC_ATTR_NONNULL_ALL +{ + PtyProcess *ptyproc = wait_eof_timer->data; + Process *proc = (Process *)ptyproc; + + if (!proc->out || !uv_is_readable(proc->out->uvstream)) { + uv_timer_stop(&ptyproc->wait_eof_timer); + pty_process_finish2(ptyproc); + } +} + +static void pty_process_finish2(PtyProcess *ptyproc) + FUNC_ATTR_NONNULL_ALL +{ + Process *proc = (Process *)ptyproc; + + UnregisterWaitEx(ptyproc->finish_wait, NULL); + uv_close((uv_handle_t *)&ptyproc->wait_eof_timer, NULL); + + DWORD exit_code = 0; + GetExitCodeProcess(ptyproc->process_handle, &exit_code); + proc->status = (int)exit_code; + + CloseHandle(ptyproc->process_handle); + ptyproc->process_handle = NULL; + + proc->internal_exit_cb(proc); +} + +/// Build the command line to pass to CreateProcessW. +/// +/// @param[in] argv Array with string arguments. +/// @param[out] cmd_line Location where saved builded cmd line. +/// +/// @returns zero on success, or error code of MultiByteToWideChar function. +/// +static int build_cmd_line(char **argv, wchar_t **cmd_line, bool is_cmdexe) + FUNC_ATTR_NONNULL_ALL +{ + size_t utf8_cmd_line_len = 0; + size_t argc = 0; + QUEUE args_q; + + QUEUE_INIT(&args_q); + while (*argv) { + size_t buf_len = is_cmdexe ? (strlen(*argv) + 1) : (strlen(*argv) * 2 + 3); + ArgNode *arg_node = xmalloc(sizeof(*arg_node)); + arg_node->arg = xmalloc(buf_len); + if (is_cmdexe) { + xstrlcpy(arg_node->arg, *argv, buf_len); + } else { + quote_cmd_arg(arg_node->arg, buf_len, *argv); + } + utf8_cmd_line_len += strlen(arg_node->arg); + QUEUE_INIT(&arg_node->node); + QUEUE_INSERT_TAIL(&args_q, &arg_node->node); + argc++; + argv++; + } + + utf8_cmd_line_len += argc; + char *utf8_cmd_line = xmalloc(utf8_cmd_line_len); + *utf8_cmd_line = NUL; + while (1) { + QUEUE *head = QUEUE_HEAD(&args_q); + QUEUE_REMOVE(head); + ArgNode *arg_node = QUEUE_DATA(head, ArgNode, node); + xstrlcat(utf8_cmd_line, arg_node->arg, utf8_cmd_line_len); + xfree(arg_node->arg); + xfree(arg_node); + if (QUEUE_EMPTY(&args_q)) { + break; + } else { + xstrlcat(utf8_cmd_line, " ", utf8_cmd_line_len); + } + } + + int result = utf8_to_utf16(utf8_cmd_line, cmd_line); + xfree(utf8_cmd_line); + return result; +} + +/// Emulate quote_cmd_arg of libuv and quotes command line argument. +/// Most of the code came from libuv. +/// +/// @param[out] dest Location where saved quotes argument. +/// @param dest_remaining Destination buffer size. +/// @param[in] src Pointer to argument. +/// +static void quote_cmd_arg(char *dest, size_t dest_remaining, const char *src) + FUNC_ATTR_NONNULL_ALL +{ + size_t src_len = strlen(src); + bool quote_hit = true; + char *start = dest; + + if (src_len == 0) { + // Need double quotation for empty argument. + snprintf(dest, dest_remaining, "\"\""); + return; + } + + if (NULL == strpbrk(src, " \t\"")) { + // No quotation needed. + xstrlcpy(dest, src, dest_remaining); + return; + } + + if (NULL == strpbrk(src, "\"\\")) { + // No embedded double quotes or backlashes, so I can just wrap quote marks. + // around the whole thing. + snprintf(dest, dest_remaining, "\"%s\"", src); + return; + } + + // Expected input/output: + // input : hello"world + // output: "hello\"world" + // input : hello""world + // output: "hello\"\"world" + // input : hello\world + // output: hello\world + // input : hello\\world + // output: hello\\world + // input : hello\"world + // output: "hello\\\"world" + // input : hello\\"world + // output: "hello\\\\\"world" + // input : hello world\ + // output: "hello world\\" + + assert(dest_remaining--); + *(dest++) = NUL; + assert(dest_remaining--); + *(dest++) = '"'; + for (size_t i = src_len; i > 0; i--) { + assert(dest_remaining--); + *(dest++) = src[i - 1]; + if (quote_hit && src[i - 1] == '\\') { + assert(dest_remaining--); + *(dest++) = '\\'; + } else if (src[i - 1] == '"') { + quote_hit = true; + assert(dest_remaining--); + *(dest++) = '\\'; + } else { + quote_hit = false; + } + } + assert(dest_remaining); + *dest = '"'; + + while (start < dest) { + char tmp = *start; + *start = *dest; + *dest = tmp; + start++; + dest--; + } +} + +/// Translate winpty error code to libuv error. +/// +/// @param[in] winpty_errno Winpty error code returned by winpty_error_code +/// function. +/// +/// @returns Error code of libuv error. +int translate_winpty_error(int winpty_errno) +{ + if (winpty_errno <= 0) { + return winpty_errno; // If < 0 then it's already a libuv error. + } + + switch (winpty_errno) { + case WINPTY_ERROR_OUT_OF_MEMORY: return UV_ENOMEM; + case WINPTY_ERROR_SPAWN_CREATE_PROCESS_FAILED: return UV_EAI_FAIL; + case WINPTY_ERROR_LOST_CONNECTION: return UV_ENOTCONN; + case WINPTY_ERROR_AGENT_EXE_MISSING: return UV_ENOENT; + case WINPTY_ERROR_UNSPECIFIED: return UV_UNKNOWN; + case WINPTY_ERROR_AGENT_DIED: return UV_ESRCH; + case WINPTY_ERROR_AGENT_TIMEOUT: return UV_ETIMEDOUT; + case WINPTY_ERROR_AGENT_CREATION_FAILED: return UV_EAI_FAIL; + default: return UV_UNKNOWN; + } +} diff --git a/src/nvim/os/pty_process_win.h b/src/nvim/os/pty_process_win.h index 8e2b37a1c1..1a4019e654 100644 --- a/src/nvim/os/pty_process_win.h +++ b/src/nvim/os/pty_process_win.h @@ -1,20 +1,27 @@ #ifndef NVIM_OS_PTY_PROCESS_WIN_H #define NVIM_OS_PTY_PROCESS_WIN_H -#include "nvim/event/libuv_process.h" +#include <uv.h> +#include <winpty.h> + +#include "nvim/event/process.h" +#include "nvim/lib/queue.h" typedef struct pty_process { Process process; char *term_name; uint16_t width, height; + winpty_t *winpty_object; + HANDLE finish_wait; + HANDLE process_handle; + uv_timer_t wait_eof_timer; } PtyProcess; -#define pty_process_spawn(job) libuv_process_spawn((LibuvProcess *)job) -#define pty_process_close(job) libuv_process_close((LibuvProcess *)job) -#define pty_process_close_master(job) libuv_process_close((LibuvProcess *)job) -#define pty_process_resize(job, width, height) ( \ - (void)job, (void)width, (void)height, 0) -#define pty_process_teardown(loop) ((void)loop, 0) +// Structure used by build_cmd_line() +typedef struct arg_node { + char *arg; // pointer to argument. + QUEUE node; // QUEUE structure. +} ArgNode; static inline PtyProcess pty_process_init(Loop *loop, void *data) { @@ -23,7 +30,14 @@ static inline PtyProcess pty_process_init(Loop *loop, void *data) rv.term_name = NULL; rv.width = 80; rv.height = 24; + rv.winpty_object = NULL; + rv.finish_wait = NULL; + rv.process_handle = NULL; return rv; } +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "os/pty_process_win.h.generated.h" +#endif + #endif // NVIM_OS_PTY_PROCESS_WIN_H diff --git a/src/nvim/screen.c b/src/nvim/screen.c index f8d519ab36..95973354bc 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -7440,13 +7440,7 @@ int number_width(win_T *wp) return n; } -/* - * Set size of the Vim shell. - * If 'mustset' is TRUE, we must set Rows and Columns, do not get the real - * window size (this is used for the :win command). - * If 'mustset' is FALSE, we may try to get the real window size and if - * it fails use 'width' and 'height'. - */ +/// Set dimensions of the Nvim application "shell". void screen_resize(int width, int height) { static int busy = FALSE; @@ -7531,8 +7525,8 @@ void screen_resize(int width, int height) --busy; } -// Check if the new shell size is valid, correct it if it's too small or way -// too big. +/// Check if the new Nvim application "shell" dimensions are valid. +/// Correct it if it's too small or way too big. void check_shellsize(void) { if (Rows < min_rows()) { diff --git a/src/nvim/testdir/Makefile b/src/nvim/testdir/Makefile index 7f1e25900b..96de7224c5 100644 --- a/src/nvim/testdir/Makefile +++ b/src/nvim/testdir/Makefile @@ -207,7 +207,7 @@ nolog: # New style of tests uses Vim script with assert calls. These are easier # to write and a lot easier to read and debug. # Limitation: Only works with the +eval feature. -RUN_VIMTEST = VIMRUNTIME=$(SCRIPTSOURCE); export VIMRUNTIME; $(VALGRIND) $(NVIM_PRG) -u unix.vim -U NONE --headless --noplugin +RUN_VIMTEST = VIMRUNTIME=$(SCRIPTSOURCE); export VIMRUNTIME; $(TOOL) $(NVIM_PRG) -u unix.vim -U NONE --headless --noplugin newtests: newtestssilent @/bin/sh -c "if test -f messages && grep -q 'FAILED' messages; then \ diff --git a/src/nvim/testdir/test_diffmode.vim b/src/nvim/testdir/test_diffmode.vim index 5de394de8e..f40e06ff33 100644 --- a/src/nvim/testdir/test_diffmode.vim +++ b/src/nvim/testdir/test_diffmode.vim @@ -218,3 +218,20 @@ func Test_diffoff() bwipe! bwipe! endfunc + +func Test_setting_cursor() + new Xtest1 + put =range(1,90) + wq + new Xtest2 + put =range(1,100) + wq + + tabe Xtest2 + $ + diffsp Xtest1 + tabclose + + call delete('Xtest1') + call delete('Xtest2') +endfunc diff --git a/src/nvim/testdir/test_window_cmd.vim b/src/nvim/testdir/test_window_cmd.vim index 188a7ed0f3..9d61921988 100644 --- a/src/nvim/testdir/test_window_cmd.vim +++ b/src/nvim/testdir/test_window_cmd.vim @@ -336,6 +336,50 @@ func Test_window_width() bw Xa Xb Xc endfunc +func Test_equalalways_on_close() + set equalalways + vsplit + windo split + split + wincmd J + " now we have a frame top-left with two windows, a frame top-right with two + " windows and a frame at the bottom, full-width. + let height_1 = winheight(1) + let height_2 = winheight(2) + let height_3 = winheight(3) + let height_4 = winheight(4) + " closing the bottom window causes all windows to be resized. + close + call assert_notequal(height_1, winheight(1)) + call assert_notequal(height_2, winheight(2)) + call assert_notequal(height_3, winheight(3)) + call assert_notequal(height_4, winheight(4)) + call assert_equal(winheight(1), winheight(3)) + call assert_equal(winheight(2), winheight(4)) + + 1wincmd w + split + 4wincmd w + resize + 5 + " left column has three windows, equalized heights. + " right column has two windows, top one a bit higher + let height_1 = winheight(1) + let height_2 = winheight(2) + let height_4 = winheight(4) + let height_5 = winheight(5) + 3wincmd w + " closing window in left column equalizes heights in left column but not in + " the right column + close + call assert_notequal(height_1, winheight(1)) + call assert_notequal(height_2, winheight(2)) + call assert_equal(height_4, winheight(3)) + call assert_equal(height_5, winheight(4)) + + only + set equalalways& +endfunc + func Test_window_jump_tag() help /iccf diff --git a/src/nvim/ui.c b/src/nvim/ui.c index a60c061949..b85a01814d 100644 --- a/src/nvim/ui.c +++ b/src/nvim/ui.c @@ -193,7 +193,12 @@ void ui_refresh(void) } row = col = 0; + + int save_p_lz = p_lz; + p_lz = false; // convince redrawing() to return true ... screen_resize(width, height); + p_lz = save_p_lz; + for (UIWidget i = 0; (int)i < UI_WIDGETS; i++) { ui_set_external(i, ext_widgets[i]); } diff --git a/src/nvim/version.c b/src/nvim/version.c index f4984864f3..f5b45caefc 100644 --- a/src/nvim/version.c +++ b/src/nvim/version.c @@ -273,8 +273,8 @@ static const int included_patches[] = { // 682, // 681, // 680, - // 679, - // 678, + 679, + 678, // 677, // 676, // 675, @@ -778,7 +778,7 @@ static const int included_patches[] = { 177, 176, // 175, - // 174, + 174, // 173 NA 172, // 171, @@ -908,7 +908,7 @@ static const int included_patches[] = { 47, 46, // 45 NA - // 44, + 44, 43, 42, 41, diff --git a/src/nvim/window.c b/src/nvim/window.c index faf5bceb56..081fc98816 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -1878,6 +1878,7 @@ int win_close(win_T *win, int free_buf) int dir; int help_window = FALSE; tabpage_T *prev_curtab = curtab; + frame_T *win_frame = win->w_frame->fr_parent; if (last_window()) { EMSG(_("E444: Cannot close last window")); @@ -2027,7 +2028,9 @@ int win_close(win_T *win, int free_buf) check_cursor(); } if (p_ea && (*p_ead == 'b' || *p_ead == dir)) { - win_equal(curwin, true, dir); + // If the frame of the closed window contains the new current window, + // only resize that frame. Otherwise resize all windows. + win_equal(curwin, curwin->w_frame->fr_parent == win_frame, dir); } else { win_comp_pos(); } diff --git a/test/functional/eval/input_spec.lua b/test/functional/eval/input_spec.lua index 74ad32bc6c..5ae23e17d0 100644 --- a/test/functional/eval/input_spec.lua +++ b/test/functional/eval/input_spec.lua @@ -23,10 +23,41 @@ before_each(function() function CustomListCompl(...) return ['FOO'] endfunction + + highlight RBP1 guibg=Red + highlight RBP2 guibg=Yellow + highlight RBP3 guibg=Green + highlight RBP4 guibg=Blue + let g:NUM_LVLS = 4 + function Redraw() + redraw! + return '' + endfunction + cnoremap <expr> {REDRAW} Redraw() + function RainBowParens(cmdline) + let ret = [] + let i = 0 + let lvl = 0 + while i < len(a:cmdline) + if a:cmdline[i] is# '(' + call add(ret, [i, i + 1, 'RBP' . ((lvl % g:NUM_LVLS) + 1)]) + let lvl += 1 + elseif a:cmdline[i] is# ')' + let lvl -= 1 + call add(ret, [i, i + 1, 'RBP' . ((lvl % g:NUM_LVLS) + 1)]) + endif + let i += 1 + endwhile + return ret + endfunction ]]) screen:set_default_attr_ids({ EOB={bold = true, foreground = Screen.colors.Blue1}, T={foreground=Screen.colors.Red}, + RBP1={background=Screen.colors.Red}, + RBP2={background=Screen.colors.Yellow}, + RBP3={background=Screen.colors.Green}, + RBP4={background=Screen.colors.Blue}, }) end) @@ -196,6 +227,18 @@ describe('input()', function() eq('Vim(call):E118: Too many arguments for function: input', exc_exec('call input("prompt> ", "default", "file", "extra")')) end) + it('supports highlighting', function() + command('nnoremap <expr> X input({"highlight": "RainBowParens"})[-1]') + feed([[X]]) + feed('(())') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {RBP1:(}{RBP2:()}{RBP1:)}^ | + ]]) + end) end) describe('inputdialog()', function() it('works with multiline prompts', function() @@ -363,4 +406,16 @@ describe('inputdialog()', function() eq('Vim(call):E118: Too many arguments for function: inputdialog', exc_exec('call inputdialog("prompt> ", "default", "file", "extra")')) end) + it('supports highlighting', function() + command('nnoremap <expr> X inputdialog({"highlight": "RainBowParens"})[-1]') + feed([[X]]) + feed('(())') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {RBP1:(}{RBP2:()}{RBP1:)}^ | + ]]) + end) end) diff --git a/test/functional/fixtures/tty-test.c b/test/functional/fixtures/tty-test.c index 7ba21d652a..edcbe23f86 100644 --- a/test/functional/fixtures/tty-test.c +++ b/test/functional/fixtures/tty-test.c @@ -5,42 +5,45 @@ #include <stdio.h> #include <stdlib.h> #include <uv.h> +#ifdef _WIN32 +# include <windows.h> +#else +# include <unistd.h> +#endif // -V:STRUCT_CAST:641 #define STRUCT_CAST(Type, obj) ((Type *)(obj)) +#define is_terminal(stream) (uv_guess_handle(fileno(stream)) == UV_TTY) +#define BUF_SIZE 0xfff +#define CTRL_C 0x03 uv_tty_t tty; +uv_tty_t tty_out; -#ifdef _WIN32 -#include <windows.h> bool owns_tty(void) { - HWND consoleWnd = GetConsoleWindow(); - DWORD dwProcessId; - GetWindowThreadProcessId(consoleWnd, &dwProcessId); - return GetCurrentProcessId() == dwProcessId; -} +#ifdef _WIN32 + // XXX: We need to make proper detect owns tty + // HWND consoleWnd = GetConsoleWindow(); + // DWORD dwProcessId; + // GetWindowThreadProcessId(consoleWnd, &dwProcessId); + // return GetCurrentProcessId() == dwProcessId; + return true; #else -#include <unistd.h> -bool owns_tty(void) -{ return getsid(0) == getpid(); -} #endif +} -#define is_terminal(stream) (uv_guess_handle(fileno(stream)) == UV_TTY) -#define BUF_SIZE 0xfff - -static void walk_cb(uv_handle_t *handle, void *arg) { +static void walk_cb(uv_handle_t *handle, void *arg) +{ if (!uv_is_closing(handle)) { uv_close(handle, NULL); } } -#ifndef WIN32 static void sig_handler(int signum) { - switch(signum) { + switch (signum) { case SIGWINCH: { int width, height; uv_tty_get_winsize(&tty, &width, &height); @@ -54,15 +57,15 @@ static void sig_handler(int signum) return; } } -#endif -// static void sigwinch_cb(uv_signal_t *handle, int signum) -// { -// int width, height; -// uv_tty_t *tty = handle->data; -// uv_tty_get_winsize(tty, &width, &height); -// fprintf(stderr, "rows: %d, cols: %d\n", height, width); -// } +#ifdef WIN32 +static void sigwinch_cb(uv_signal_t *handle, int signum) +{ + int width, height; + uv_tty_get_winsize(&tty_out, &width, &height); + fprintf(stderr, "rows: %d, cols: %d\n", height, width); +} +#endif static void alloc_cb(uv_handle_t *handle, size_t suggested, uv_buf_t *buf) { @@ -80,7 +83,7 @@ static void read_cb(uv_stream_t *stream, ssize_t cnt, const uv_buf_t *buf) int *interrupted = stream->data; for (int i = 0; i < cnt; i++) { - if (buf->base[i] == 3) { + if (buf->base[i] == CTRL_C) { (*interrupted)++; } } @@ -88,11 +91,13 @@ static void read_cb(uv_stream_t *stream, ssize_t cnt, const uv_buf_t *buf) uv_loop_t write_loop; uv_loop_init(&write_loop); uv_tty_t out; - uv_tty_init(&write_loop, &out, 1, 0); + uv_tty_init(&write_loop, &out, fileno(stdout), 0); + uv_write_t req; uv_buf_t b = {.base = buf->base, .len = (size_t)cnt}; uv_write(&req, STRUCT_CAST(uv_stream_t, &out), &b, 1, NULL); uv_run(&write_loop, UV_RUN_DEFAULT); + uv_close(STRUCT_CAST(uv_handle_t, &out), NULL); uv_run(&write_loop, UV_RUN_DEFAULT); if (uv_loop_close(&write_loop)) { @@ -137,7 +142,7 @@ int main(int argc, char **argv) if (argc > 1) { int count = atoi(argv[1]); - for (int i = 0; i < count; ++i) { + for (int i = 0; i < count; i++) { printf("line%d\n", i); } fflush(stdout); @@ -148,8 +153,14 @@ int main(int argc, char **argv) uv_prepare_t prepare; uv_prepare_init(uv_default_loop(), &prepare); uv_prepare_start(&prepare, prepare_cb); - // uv_tty_t tty; +#ifndef WIN32 uv_tty_init(uv_default_loop(), &tty, fileno(stderr), 1); +#else + uv_tty_init(uv_default_loop(), &tty, fileno(stdin), 1); + uv_tty_init(uv_default_loop(), &tty_out, fileno(stdout), 0); + int width, height; + uv_tty_get_winsize(&tty_out, &width, &height); +#endif uv_tty_set_mode(&tty, UV_TTY_MODE_RAW); tty.data = &interrupted; uv_read_start(STRUCT_CAST(uv_stream_t, &tty), alloc_cb, read_cb); @@ -160,15 +171,17 @@ int main(int argc, char **argv) sa.sa_handler = sig_handler; sigaction(SIGHUP, &sa, NULL); sigaction(SIGWINCH, &sa, NULL); - // uv_signal_t sigwinch_watcher; - // uv_signal_init(uv_default_loop(), &sigwinch_watcher); - // sigwinch_watcher.data = &tty; - // uv_signal_start(&sigwinch_watcher, sigwinch_cb, SIGWINCH); +#else + uv_signal_t sigwinch_watcher; + uv_signal_init(uv_default_loop(), &sigwinch_watcher); + uv_signal_start(&sigwinch_watcher, sigwinch_cb, SIGWINCH); #endif uv_run(uv_default_loop(), UV_RUN_DEFAULT); +#ifndef WIN32 // XXX: Without this the SIGHUP handler is skipped on some systems. sleep(100); +#endif return 0; } diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua index 48b8512bf0..4ce33fef7b 100644 --- a/test/functional/terminal/buffer_spec.lua +++ b/test/functional/terminal/buffer_spec.lua @@ -6,8 +6,6 @@ local eval, feed_command, source = helpers.eval, helpers.feed_command, helpers.s local eq, neq = helpers.eq, helpers.neq local write_file = helpers.write_file -if helpers.pending_win32(pending) then return end - describe('terminal buffer', function() local screen @@ -160,6 +158,7 @@ describe('terminal buffer', function() end) it('handles loss of focus gracefully', function() + if helpers.pending_win32(pending) then return end -- Change the statusline to avoid printing the file name, which varies. nvim('set_option', 'statusline', '==========') feed_command('set laststatus=0') @@ -205,7 +204,6 @@ describe('terminal buffer', function() end) describe('No heap-buffer-overflow when using', function() - local testfilename = 'Xtestfile-functional-terminal-buffers_spec' before_each(function() diff --git a/test/functional/terminal/cursor_spec.lua b/test/functional/terminal/cursor_spec.lua index 84d0322f12..d49f1bfc23 100644 --- a/test/functional/terminal/cursor_spec.lua +++ b/test/functional/terminal/cursor_spec.lua @@ -7,8 +7,6 @@ local feed_command = helpers.feed_command local hide_cursor = thelpers.hide_cursor local show_cursor = thelpers.show_cursor -if helpers.pending_win32(pending) then return end - describe('terminal cursor', function() local screen diff --git a/test/functional/terminal/ex_terminal_spec.lua b/test/functional/terminal/ex_terminal_spec.lua index be0fd9f8ff..9930efc402 100644 --- a/test/functional/terminal/ex_terminal_spec.lua +++ b/test/functional/terminal/ex_terminal_spec.lua @@ -3,10 +3,10 @@ local Screen = require('test.functional.ui.screen') local clear, wait, nvim = helpers.clear, helpers.wait, helpers.nvim local nvim_dir, source, eq = helpers.nvim_dir, helpers.source, helpers.eq local feed_command, eval = helpers.feed_command, helpers.eval - -if helpers.pending_win32(pending) then return end +local iswin = helpers.iswin describe(':terminal', function() + if helpers.pending_win32(pending) then return end local screen before_each(function() @@ -174,12 +174,15 @@ describe(':terminal (with fake shell)', function() eq('term://', string.match(eval('bufname("%")'), "^term://")) helpers.feed([[<C-\><C-N>]]) feed_command([[find */shadacat.py]]) - eq('scripts/shadacat.py', eval('bufname("%")')) + if iswin() then + eq('scripts\\shadacat.py', eval('bufname("%")')) + else + eq('scripts/shadacat.py', eval('bufname("%")')) + end end) it('works with gf', function() terminal_with_fake_shell([[echo "scripts/shadacat.py"]]) - wait() screen:expect([[ ready $ echo "scripts/shadacat.py" | | diff --git a/test/functional/terminal/helpers.lua b/test/functional/terminal/helpers.lua index 3b04d17705..bd24b9785d 100644 --- a/test/functional/terminal/helpers.lua +++ b/test/functional/terminal/helpers.lua @@ -33,7 +33,6 @@ local function disable_mouse() feed_termcode('[?1002l') end local default_command = '["'..nvim_dir..'/tty-test'..'"]' - local function screen_setup(extra_rows, command, cols) extra_rows = extra_rows and extra_rows or 0 command = command and command or default_command diff --git a/test/functional/terminal/highlight_spec.lua b/test/functional/terminal/highlight_spec.lua index bb40770235..fddc0bbb71 100644 --- a/test/functional/terminal/highlight_spec.lua +++ b/test/functional/terminal/highlight_spec.lua @@ -5,8 +5,6 @@ local feed, clear, nvim = helpers.feed, helpers.clear, helpers.nvim local nvim_dir, command = helpers.nvim_dir, helpers.command local eq, eval = helpers.eq, helpers.eval -if helpers.pending_win32(pending) then return end - describe('terminal window highlighting', function() local screen @@ -55,6 +53,7 @@ describe('terminal window highlighting', function() end) local function pass_attrs() + if helpers.pending_win32(pending) then return end screen:expect(sub([[ tty ready | {NUM:text}text{10: } | @@ -69,6 +68,7 @@ describe('terminal window highlighting', function() it('will pass the corresponding attributes', pass_attrs) it('will pass the corresponding attributes on scrollback', function() + if helpers.pending_win32(pending) then return end pass_attrs() local lines = {} for i = 1, 8 do @@ -145,6 +145,7 @@ describe('terminal window highlighting with custom palette', function() end) it('will use the custom color', function() + if helpers.pending_win32(pending) then return end thelpers.set_fg(3) thelpers.feed_data('text') thelpers.clear_attrs() diff --git a/test/functional/terminal/mouse_spec.lua b/test/functional/terminal/mouse_spec.lua index 9239c2ad31..29c62d7be7 100644 --- a/test/functional/terminal/mouse_spec.lua +++ b/test/functional/terminal/mouse_spec.lua @@ -4,8 +4,6 @@ local clear, eq, eval = helpers.clear, helpers.eq, helpers.eval local feed, nvim = helpers.feed, helpers.nvim local feed_data = thelpers.feed_data -if helpers.pending_win32(pending) then return end - describe('terminal mouse', function() local screen @@ -67,6 +65,7 @@ describe('terminal mouse', function() end) it('will forward mouse clicks to the program', function() + if helpers.pending_win32(pending) then return end feed('<LeftMouse><1,2>') screen:expect([[ line27 | @@ -80,6 +79,7 @@ describe('terminal mouse', function() end) it('will forward mouse scroll to the program', function() + if helpers.pending_win32(pending) then return end feed('<ScrollWheelUp><0,0>') screen:expect([[ line27 | @@ -94,6 +94,7 @@ describe('terminal mouse', function() end) describe('with a split window and other buffer', function() + if helpers.pending_win32(pending) then return end before_each(function() feed('<c-\\><c-n>:vsp<cr>') screen:expect([[ diff --git a/test/functional/terminal/scrollback_spec.lua b/test/functional/terminal/scrollback_spec.lua index 05f81295c2..af9b414311 100644 --- a/test/functional/terminal/scrollback_spec.lua +++ b/test/functional/terminal/scrollback_spec.lua @@ -3,6 +3,7 @@ local helpers = require('test.functional.helpers')(after_each) local thelpers = require('test.functional.terminal.helpers') local clear, eq, curbuf = helpers.clear, helpers.eq, helpers.curbuf local feed, nvim_dir, feed_command = helpers.feed, helpers.nvim_dir, helpers.feed_command +local iswin = helpers.iswin local eval = helpers.eval local command = helpers.command local wait = helpers.wait @@ -11,8 +12,6 @@ local curbufmeths = helpers.curbufmeths local nvim = helpers.nvim local feed_data = thelpers.feed_data -if helpers.pending_win32(pending) then return end - describe('terminal scrollback', function() local screen @@ -58,7 +57,7 @@ describe('terminal scrollback', function() end) end) - describe('with the cursor at the last row', function() + describe('with cursor at last row', function() before_each(function() feed_data({'line1', 'line2', 'line3', 'line4', ''}) screen:expect([[ @@ -139,16 +138,18 @@ describe('terminal scrollback', function() end) - describe('and the height is decreased by 1', function() + describe('and height decreased by 1', function() + if helpers.pending_win32(pending) then return end local function will_hide_top_line() - screen:try_resize(screen._width, screen._height - 1) + feed([[<C-\><C-N>:]]) -- Go to cmdline-mode, so cursor is at bottom. + screen:try_resize(screen._width - 2, screen._height - 1) screen:expect([[ - line2 | - line3 | - line4 | - rows: 5, cols: 30 | - {1: } | - {3:-- TERMINAL --} | + line2 | + line3 | + line4 | + rows: 5, cols: 28 | + {2: } | + :^ | ]]) end @@ -157,23 +158,23 @@ describe('terminal scrollback', function() describe('and then decreased by 2', function() before_each(function() will_hide_top_line() - screen:try_resize(screen._width, screen._height - 2) + screen:try_resize(screen._width - 2, screen._height - 2) end) it('will hide the top 3 lines', function() screen:expect([[ - rows: 5, cols: 30 | - rows: 3, cols: 30 | - {1: } | - {3:-- TERMINAL --} | + rows: 5, cols: 28 | + rows: 3, cols: 26 | + {2: } | + :^ | ]]) eq(8, curbuf('line_count')) - feed('<c-\\><c-n>3k') + feed([[<C-\><C-N>3k]]) screen:expect([[ - ^line4 | - rows: 5, cols: 30 | - rows: 3, cols: 30 | - | + ^line4 | + rows: 5, cols: 28 | + rows: 3, cols: 26 | + | ]]) end) end) @@ -181,6 +182,11 @@ describe('terminal scrollback', function() end) describe('with empty lines after the cursor', function() + -- XXX: Can't test this reliably on Windows unless the cursor is _moved_ + -- by the resize. http://docs.libuv.org/en/v1.x/signal.html + -- See also: https://github.com/rprichard/winpty/issues/110 + if helpers.pending_win32(pending) then return end + describe('and the height is decreased by 2', function() before_each(function() screen:try_resize(screen._width, screen._height - 2) @@ -255,6 +261,10 @@ describe('terminal scrollback', function() end) describe('and the height is increased by 1', function() + -- XXX: Can't test this reliably on Windows unless the cursor is _moved_ + -- by the resize. http://docs.libuv.org/en/v1.x/signal.html + -- See also: https://github.com/rprichard/winpty/issues/110 + if helpers.pending_win32(pending) then return end local function pop_then_push() screen:try_resize(screen._width, screen._height + 1) screen:expect([[ @@ -384,10 +394,20 @@ describe("'scrollback' option", function() end it('set to 0 behaves as 1', function() - local screen = thelpers.screen_setup(nil, "['sh']", 30) + local screen + if iswin() then + screen = thelpers.screen_setup(nil, + "['powershell.exe', '-NoLogo', '-NoProfile', '-NoExit', '-Command', 'function global:prompt {return "..'"$"'.."}']", 30) + else + screen = thelpers.screen_setup(nil, "['sh']", 30) + end curbufmeths.set_option('scrollback', 0) - feed_data('for i in $(seq 1 30); do echo "line$i"; done\n') + if iswin() then + feed_data('for($i=1;$i -le 30;$i++){Write-Host \"line$i\"}\r') + else + feed_data('for i in $(seq 1 30); do echo "line$i"; done\n') + end screen:expect('line30 ', nil, nil, nil, true) retry(nil, nil, function() expect_lines(7) end) @@ -395,7 +415,13 @@ describe("'scrollback' option", function() end) it('deletes lines (only) if necessary', function() - local screen = thelpers.screen_setup(nil, "['sh']", 30) + local screen + if iswin() then + screen = thelpers.screen_setup(nil, + "['powershell.exe', '-NoLogo', '-NoProfile', '-NoExit', '-Command', 'function global:prompt {return "..'"$"'.."}']", 30) + else + screen = thelpers.screen_setup(nil, "['sh']", 30) + end curbufmeths.set_option('scrollback', 200) @@ -403,7 +429,11 @@ describe("'scrollback' option", function() screen:expect('$', nil, nil, nil, true) wait() - feed_data('for i in $(seq 1 30); do echo "line$i"; done\n') + if iswin() then + feed_data('for($i=1;$i -le 30;$i++){Write-Host \"line$i\"}\r') + else + feed_data('for i in $(seq 1 30); do echo "line$i"; done\n') + end screen:expect('line30 ', nil, nil, nil, true) @@ -416,7 +446,11 @@ describe("'scrollback' option", function() -- Terminal job data is received asynchronously, may happen before the -- 'scrollback' option is synchronized with the internal sb_buffer. command('sleep 100m') - feed_data('for i in $(seq 1 40); do echo "line$i"; done\n') + if iswin() then + feed_data('for($i=1;$i -le 40;$i++){Write-Host \"line$i\"}\r') + else + feed_data('for i in $(seq 1 40); do echo "line$i"; done\n') + end screen:expect('line40 ', nil, nil, nil, true) diff --git a/test/functional/terminal/window_spec.lua b/test/functional/terminal/window_spec.lua index 888b1e1328..0f705cfe40 100644 --- a/test/functional/terminal/window_spec.lua +++ b/test/functional/terminal/window_spec.lua @@ -3,8 +3,6 @@ local thelpers = require('test.functional.terminal.helpers') local feed, clear = helpers.feed, helpers.clear local wait = helpers.wait -if helpers.pending_win32(pending) then return end - describe('terminal window', function() local screen diff --git a/test/functional/terminal/window_split_tab_spec.lua b/test/functional/terminal/window_split_tab_spec.lua index 4867e0d9fa..c5199f620e 100644 --- a/test/functional/terminal/window_split_tab_spec.lua +++ b/test/functional/terminal/window_split_tab_spec.lua @@ -4,8 +4,6 @@ local clear = helpers.clear local feed, nvim = helpers.feed, helpers.nvim local feed_command = helpers.feed_command -if helpers.pending_win32(pending) then return end - describe('terminal', function() local screen @@ -25,6 +23,7 @@ describe('terminal', function() end) it('resets its size when entering terminal window', function() + if helpers.pending_win32(pending) then return end feed('<c-\\><c-n>') feed_command('2split') screen:expect([[ @@ -69,31 +68,25 @@ describe('terminal', function() describe('when the screen is resized', function() it('will forward a resize request to the program', function() - screen:try_resize(screen._width + 3, screen._height + 5) + feed([[<C-\><C-N>:]]) -- Go to cmdline-mode, so cursor is at bottom. + screen:try_resize(screen._width - 3, screen._height - 2) screen:expect([[ - tty ready | - rows: 14, cols: 53 | - {1: } | - | - | - | - | - | - | - | - | - | - | - | - {3:-- TERMINAL --} | + tty ready | + rows: 7, cols: 47 | + {2: } | + | + | + | + | + :^ | ]]) - screen:try_resize(screen._width - 6, screen._height - 10) + screen:try_resize(screen._width - 6, screen._height - 3) screen:expect([[ - tty ready | - rows: 14, cols: 53 | - rows: 4, cols: 47 | - {1: } | - {3:-- TERMINAL --} | + tty ready | + rows: 7, cols: 47 | + rows: 4, cols: 41 | + {2: } | + :^ | ]]) end) end) diff --git a/test/functional/ui/cmdline_highlight_spec.lua b/test/functional/ui/cmdline_highlight_spec.lua new file mode 100644 index 0000000000..d87ce72599 --- /dev/null +++ b/test/functional/ui/cmdline_highlight_spec.lua @@ -0,0 +1,893 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') + +local eq = helpers.eq +local feed = helpers.feed +local clear = helpers.clear +local meths = helpers.meths +local funcs = helpers.funcs +local source = helpers.source +local dedent = helpers.dedent +local command = helpers.command +local curbufmeths = helpers.curbufmeths + +local screen + +-- Bug in input() handling: :redraw! will erase the whole prompt up until +-- user types something. It exists in Vim as well, so using `h<BS>` as +-- a workaround. +local function redraw_input() + feed('{REDRAW}h<BS>') +end + +before_each(function() + clear() + screen = Screen.new(40, 8) + screen:attach() + source([[ + highlight RBP1 guibg=Red + highlight RBP2 guibg=Yellow + highlight RBP3 guibg=Green + highlight RBP4 guibg=Blue + let g:NUM_LVLS = 4 + function Redraw() + redraw! + return '' + endfunction + let g:id = '' + cnoremap <expr> {REDRAW} Redraw() + function DoPrompt(do_return) abort + let id = g:id + let Cb = g:Nvim_color_input{g:id} + let out = input({'prompt': ':', 'highlight': Cb}) + let g:out{id} = out + return (a:do_return ? out : '') + endfunction + nnoremap <expr> {PROMPT} DoPrompt(0) + cnoremap <expr> {PROMPT} DoPrompt(1) + function RainBowParens(cmdline) + let ret = [] + let i = 0 + let lvl = 0 + while i < len(a:cmdline) + if a:cmdline[i] is# '(' + call add(ret, [i, i + 1, 'RBP' . ((lvl % g:NUM_LVLS) + 1)]) + let lvl += 1 + elseif a:cmdline[i] is# ')' + let lvl -= 1 + call add(ret, [i, i + 1, 'RBP' . ((lvl % g:NUM_LVLS) + 1)]) + endif + let i += 1 + endwhile + return ret + endfunction + function SplittedMultibyteStart(cmdline) + let ret = [] + let i = 0 + while i < len(a:cmdline) + let char = nr2char(char2nr(a:cmdline[i:])) + if a:cmdline[i:i + len(char) - 1] is# char + if len(char) > 1 + call add(ret, [i + 1, i + len(char), 'RBP2']) + endif + let i += len(char) + else + let i += 1 + endif + endwhile + return ret + endfunction + function SplittedMultibyteEnd(cmdline) + let ret = [] + let i = 0 + while i < len(a:cmdline) + let char = nr2char(char2nr(a:cmdline[i:])) + if a:cmdline[i:i + len(char) - 1] is# char + if len(char) > 1 + call add(ret, [i, i + 1, 'RBP1']) + endif + let i += len(char) + else + let i += 1 + endif + endwhile + return ret + endfunction + function Echoing(cmdline) + echo 'HERE' + return v:_null_list + endfunction + function Echoning(cmdline) + echon 'HERE' + return v:_null_list + endfunction + function Echomsging(cmdline) + echomsg 'HERE' + return v:_null_list + endfunction + function Echoerring(cmdline) + echoerr 'HERE' + return v:_null_list + endfunction + function Redrawing(cmdline) + redraw! + return v:_null_list + endfunction + function Throwing(cmdline) + throw "ABC" + return v:_null_list + endfunction + function Halting(cmdline) + while 1 + endwhile + endfunction + function ReturningGlobal(cmdline) + return g:callback_return + endfunction + function ReturningGlobal2(cmdline) + return g:callback_return[:len(a:cmdline)-1] + endfunction + function ReturningGlobalN(n, cmdline) + return g:callback_return{a:n} + endfunction + let g:recording_calls = [] + function Recording(cmdline) + call add(g:recording_calls, a:cmdline) + return [] + endfunction + ]]) + screen:set_default_attr_ids({ + RBP1={background = Screen.colors.Red}, + RBP2={background = Screen.colors.Yellow}, + RBP3={background = Screen.colors.Green}, + RBP4={background = Screen.colors.Blue}, + EOB={bold = true, foreground = Screen.colors.Blue1}, + ERR={foreground = Screen.colors.Grey100, background = Screen.colors.Red}, + SK={foreground = Screen.colors.Blue}, + PE={bold = true, foreground = Screen.colors.SeaGreen4} + }) +end) + +local function set_color_cb(funcname, callback_return, id) + meths.set_var('id', id or '') + if id and id ~= '' and funcs.exists('*' .. funcname .. 'N') then + command(('let g:Nvim_color_input%s = {cmdline -> %sN(%s, cmdline)}'):format( + id, funcname, id)) + if callback_return then + meths.set_var('callback_return' .. id, callback_return) + end + else + meths.set_var('Nvim_color_input', funcname) + if callback_return then + meths.set_var('callback_return', callback_return) + end + end +end +local function start_prompt(text) + feed('{PROMPT}' .. (text or '')) +end + +describe('Command-line coloring', function() + it('works', function() + set_color_cb('RainBowParens') + meths.set_option('more', false) + start_prompt() + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :^ | + ]]) + feed('e') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :e^ | + ]]) + feed('cho ') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :echo ^ | + ]]) + feed('(') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :echo {RBP1:(}^ | + ]]) + feed('(') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :echo {RBP1:(}{RBP2:(}^ | + ]]) + feed('42') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :echo {RBP1:(}{RBP2:(}42^ | + ]]) + feed('))') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :echo {RBP1:(}{RBP2:(}42{RBP2:)}{RBP1:)}^ | + ]]) + feed('<BS>') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :echo {RBP1:(}{RBP2:(}42{RBP2:)}^ | + ]]) + redraw_input() + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :echo {RBP1:(}{RBP2:(}42{RBP2:)}^ | + ]]) + end) + for _, func_part in ipairs({'', 'n', 'msg'}) do + it('disables :echo' .. func_part .. ' messages', function() + set_color_cb('Echo' .. func_part .. 'ing') + start_prompt('echo') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :echo^ | + ]]) + end) + end + it('does the right thing when hl start appears to split multibyte char', + function() + set_color_cb('SplittedMultibyteStart') + start_prompt('echo "«') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :echo " | + {ERR:E5405: Chunk 0 start 7 splits multibyte }| + {ERR:character} | + :echo "«^ | + ]]) + feed('»') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :echo " | + {ERR:E5405: Chunk 0 start 7 splits multibyte }| + {ERR:character} | + :echo "«»^ | + ]]) + end) + it('does the right thing when hl end appears to split multibyte char', + function() + set_color_cb('SplittedMultibyteEnd') + start_prompt('echo "«') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :echo " | + {ERR:E5406: Chunk 0 end 7 splits multibyte ch}| + {ERR:aracter} | + :echo "«^ | + ]]) + end) + it('does the right thing when errorring', function() + set_color_cb('Echoerring') + start_prompt('e') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + : | + {ERR:E5407: Callback has thrown an exception:}| + {ERR: Vim(echoerr):HERE} | + :e^ | + ]]) + end) + it('silences :echo', function() + set_color_cb('Echoing') + start_prompt('e') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :e^ | + ]]) + eq('', meths.command_output('messages')) + end) + it('silences :echon', function() + set_color_cb('Echoning') + start_prompt('e') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :e^ | + ]]) + eq('', meths.command_output('messages')) + end) + it('silences :echomsg', function() + set_color_cb('Echomsging') + start_prompt('e') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :e^ | + ]]) + eq('', meths.command_output('messages')) + end) + it('does the right thing when throwing', function() + set_color_cb('Throwing') + start_prompt('e') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + : | + {ERR:E5407: Callback has thrown an exception:}| + {ERR: ABC} | + :e^ | + ]]) + end) + it('stops executing callback after a number of errors', function() + set_color_cb('SplittedMultibyteStart') + start_prompt('let x = "«»«»«»«»«»"\n') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :let x = " | + {ERR:E5405: Chunk 0 start 10 splits multibyte}| + {ERR: character} | + ^:let x = "«»«»«»«»«»" | + ]]) + feed('\n') + screen:expect([[ + ^ | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + | + ]]) + eq('let x = "«»«»«»«»«»"', meths.get_var('out')) + local msg = '\nE5405: Chunk 0 start 10 splits multibyte character' + eq(msg:rep(1), funcs.execute('messages')) + end) + it('allows interrupting callback with <C-c>', function() + set_color_cb('Halting') + start_prompt('echo 42') + screen:expect([[ + ^ | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + | + ]]) + screen:sleep(500) + feed('<C-c>') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + : | + {ERR:E5407: Callback has thrown an exception:}| + {ERR: Keyboard interrupt} | + :echo 42^ | + ]]) + redraw_input() + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :echo 42^ | + ]]) + feed('\n') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + ^:echo 42 | + ]]) + feed('\n') + eq('echo 42', meths.get_var('out')) + feed('<C-c>') + screen:expect([[ + ^ | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + Type :quit<Enter> to exit Nvim | + ]]) + end) + it('works fine with NUL, NL, CR', function() + set_color_cb('RainBowParens') + start_prompt('echo ("<C-v><CR><C-v><Nul><C-v><NL>")') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :echo {RBP1:(}"{SK:^M^@^@}"{RBP1:)}^ | + ]]) + end) + it('errors out when callback returns something wrong', function() + command('cnoremap + ++') + set_color_cb('ReturningGlobal', '') + start_prompt('#') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + : | + {ERR:E5400: Callback should return list} | + :#^ | + ]]) + + feed('<CR><CR><CR>') + set_color_cb('ReturningGlobal', {{0, 1, 'Normal'}, 42}) + start_prompt('#') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + : | + {ERR:E5401: List item 1 is not a List} | + :#^ | + ]]) + + feed('<CR><CR><CR>') + set_color_cb('ReturningGlobal2', {{0, 1, 'Normal'}, {1}}) + start_prompt('+') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :+ | + {ERR:E5402: List item 1 has incorrect length:}| + {ERR: 1 /= 3} | + :++^ | + ]]) + + feed('<CR><CR><CR>') + set_color_cb('ReturningGlobal2', {{0, 1, 'Normal'}, {2, 3, 'Normal'}}) + start_prompt('+') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :+ | + {ERR:E5403: Chunk 1 start 2 not in range [1, }| + {ERR:2)} | + :++^ | + ]]) + + feed('<CR><CR><CR>') + set_color_cb('ReturningGlobal2', {{0, 1, 'Normal'}, {1, 3, 'Normal'}}) + start_prompt('+') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :+ | + {ERR:E5404: Chunk 1 end 3 not in range (1, 2]}| + | + :++^ | + ]]) + end) + it('does not error out when called from a errorred out cycle', function() + set_color_cb('ReturningGlobal', {{0, 1, 'Normal'}}) + feed(dedent([[ + :set regexpengine=2 + :for pat in [' \ze*', ' \zs*'] + : try + : let l = matchlist('x x', pat) + : $put =input({'prompt':'>','highlight':'ReturningGlobal'}) + : + : $put ='E888 NOT detected for ' . pat + : catch + : $put =input({'prompt':'>','highlight':'ReturningGlobal'}) + : + : $put ='E888 detected for ' . pat + : endtry + :endfor + : + : + : + : + : + : + ]])) + eq({'', ':', 'E888 detected for \\ze*', ':', 'E888 detected for \\zs*'}, + curbufmeths.get_lines(0, -1, false)) + eq('', funcs.execute('messages')) + end) + it('allows nesting input()s', function() + set_color_cb('ReturningGlobal', {{0, 1, 'RBP1'}}, '') + start_prompt('1') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :{RBP1:1}^ | + ]]) + + set_color_cb('ReturningGlobal', {{0, 1, 'RBP2'}}, '1') + start_prompt('2') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :{RBP2:2}^ | + ]]) + + set_color_cb('ReturningGlobal', {{0, 1, 'RBP3'}}, '2') + start_prompt('3') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :{RBP3:3}^ | + ]]) + + set_color_cb('ReturningGlobal', {{0, 1, 'RBP4'}}, '3') + start_prompt('4') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :{RBP4:4}^ | + ]]) + + feed('<CR>') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :{RBP3:3}4^ | + ]]) + feed('<CR>') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :{RBP2:2}34^ | + ]]) + feed('<CR>') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :{RBP1:1}234^ | + ]]) + feed('<CR><CR><C-l>') + screen:expect([[ + ^ | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + | + ]]) + eq('1234', meths.get_var('out')) + eq('234', meths.get_var('out1')) + eq('34', meths.get_var('out2')) + eq('4', meths.get_var('out3')) + eq(0, funcs.exists('g:out4')) + end) + it('runs callback with the same data only once', function() + local function new_recording_calls(...) + eq({...}, meths.get_var('recording_calls')) + meths.set_var('recording_calls', {}) + end + set_color_cb('Recording') + start_prompt('') + -- Regression test. Disambiguation: + -- + -- new_recording_calls(expected_result) -- (actual_before_fix) + -- + feed('a') + new_recording_calls('a') -- ('a', 'a') + feed('b') + new_recording_calls('ab') -- ('a', 'ab', 'ab') + feed('c') + new_recording_calls('abc') -- ('ab', 'abc', 'abc') + feed('<BS>') + new_recording_calls('ab') -- ('abc', 'ab', 'ab') + feed('<BS>') + new_recording_calls('a') -- ('ab', 'a', 'a') + feed('<BS>') + new_recording_calls() -- ('a') + feed('<CR><CR>') + eq('', meths.get_var('out')) + end) +end) +describe('Ex commands coloring support', function() + it('works', function() + meths.set_var('Nvim_color_cmdline', 'RainBowParens') + feed(':echo (((1)))') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :echo {RBP1:(}{RBP2:(}{RBP3:(}1{RBP3:)}{RBP2:)}{RBP1:)}^ | + ]]) + end) + it('still executes command-line even if errored out', function() + meths.set_var('Nvim_color_cmdline', 'SplittedMultibyteStart') + feed(':let x = "«"\n') + eq('«', meths.get_var('x')) + local msg = 'E5405: Chunk 0 start 10 splits multibyte character' + eq('\n'..msg, funcs.execute('messages')) + end) + it('does not error out when called from a errorred out cycle', function() + -- Apparently when there is a cycle in which one of the commands errors out + -- this error may be caught by color_cmdline before it is presented to the + -- user. + feed(dedent([[ + :set regexpengine=2 + :for pat in [' \ze*', ' \zs*'] + : try + : let l = matchlist('x x', pat) + : $put ='E888 NOT detected for ' . pat + : catch + : $put ='E888 detected for ' . pat + : endtry + :endfor + ]])) + eq({'', 'E888 detected for \\ze*', 'E888 detected for \\zs*'}, + curbufmeths.get_lines(0, -1, false)) + eq('', funcs.execute('messages')) + end) + it('does not crash when using `n` in debug mode', function() + feed(':debug execute "echo 1"\n') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + Entering Debug mode. Type "cont" to con| + tinue. | + cmd: execute "echo 1" | + >^ | + ]]) + feed('n\n') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + Entering Debug mode. Type "cont" to con| + tinue. | + cmd: execute "echo 1" | + >n | + 1 | + {PE:Press ENTER or type command to continue}^ | + ]]) + feed('\n') + screen:expect([[ + ^ | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + | + ]]) + end) + it('does not prevent mapping error from cancelling prompt', function() + command("cnoremap <expr> x execute('throw 42')[-1]") + feed(':#x') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + :# | + {ERR:Error detected while processing :} | + {ERR:E605: Exception not caught: 42} | + :#^ | + ]]) + feed('<CR>') + screen:expect([[ + ^ | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + | + ]]) + feed('<CR>') + screen:expect([[ + ^ | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + | + ]]) + eq('\nError detected while processing :\nE605: Exception not caught: 42', + meths.command_output('messages')) + end) + it('errors out when failing to get callback', function() + meths.set_var('Nvim_color_cmdline', 42) + feed(':#') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + : | + {ERR:E5408: Unable to get g:Nvim_color_cmdlin}| + {ERR:e callback: Vim:E6000: Argument is not a}| + {ERR: function or function name} | + :#^ | + ]]) + end) +end) +describe('Expressions coloring support', function() + it('works', function() + meths.set_var('Nvim_color_expr', 'RainBowParens') + feed(':echo <C-r>=(((1)))') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + ={RBP1:(}{RBP2:(}{RBP3:(}1{RBP3:)}{RBP2:)}{RBP1:)}^ | + ]]) + end) + it('errors out when failing to get callback', function() + meths.set_var('Nvim_color_expr', 42) + feed(':<C-r>=1') + screen:expect([[ + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + = | + {ERR:E5409: Unable to get g:Nvim_color_expr c}| + {ERR:allback: Vim:E6000: Argument is not a fu}| + {ERR:nction or function name} | + =1^ | + ]]) + end) +end) diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua index 5408e1e195..a6b7fb2997 100644 --- a/test/functional/ui/screen.lua +++ b/test/functional/ui/screen.lua @@ -251,7 +251,7 @@ function Screen:expect(expected, attr_ids, attr_ignore, condition, any) ..'Expected:\n |'..table.concat(msg_expected_rows, '|\n |')..'|\n' ..'Actual:\n |'..table.concat(actual_rows, '|\n |')..'|\n\n'..[[ To print the expect() call that would assert the current screen state, use -screen:snaphot_util(). In case of non-deterministic failures, use +screen:snapshot_util(). In case of non-deterministic failures, use screen:redraw_debug() to show all intermediate screen states. ]]) end end |