diff options
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 | 
