Merge #6364 'command-line color hook'

This commit is contained in:
Justin M. Keyes 2017-08-16 00:20:37 +02:00
commit bb70eec177
16 changed files with 1526 additions and 96 deletions

View File

@ -4703,6 +4703,7 @@ input({opts})
cancelreturn "" Same as {cancelreturn} from cancelreturn "" Same as {cancelreturn} from
|inputdialog()|. Also works with |inputdialog()|. Also works with
input(). input().
highlight nothing Highlight handler: |Funcref|.
The highlighting set with |:echohl| is used for the prompt. The highlighting set with |:echohl| is used for the prompt.
The input is entered just like a command-line, with the same 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 "-complete=" argument. Refer to |:command-completion| for
more information. Example: > more information. Example: >
let fname = input("File: ", "", "file") 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 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). the versions that only run in GUI mode (e.g., the Win32 GUI).
Note: When input() is called from within a mapping it will Note: When input() is called from within a mapping it will

View File

@ -129,7 +129,6 @@ Commands:
Functions: Functions:
|dictwatcheradd()| notifies a callback whenever a |Dict| is modified |dictwatcheradd()| notifies a callback whenever a |Dict| is modified
|dictwatcherdel()| |dictwatcherdel()|
|execute()| works with |:redir|
|menu_get()| |menu_get()|
|msgpackdump()|, |msgpackparse()| provide msgpack de/serialization |msgpackdump()|, |msgpackparse()| provide msgpack de/serialization
@ -147,6 +146,14 @@ Highlight groups:
|hl-TermCursorNC| |hl-TermCursorNC|
|hl-Whitespace| highlights 'listchars' whitespace |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* 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| |system()| does not support writing/reading "backgrounded" commands. |E5677|
|:redir| nested in |execute()| works.
Nvim may throttle (skip) messages from shell commands (|:!|, |:grep|, |:make|) 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 if there is too much output. No data is lost, this only affects display and
makes things faster. |:terminal| output is never throttled. 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 on cancel and completion respectively) via dictionary argument (replaces all
other arguments if used). other arguments if used).
|input()| and |inputdialog()| now support user-defined cmdline highlighting.
============================================================================== ==============================================================================
5. Missing legacy features *nvim-features-missing* 5. Missing legacy features *nvim-features-missing*

View File

@ -37,7 +37,72 @@ typedef struct {
# include "api/private/ui_events_metadata.generated.h" # include "api/private/ui_events_metadata.generated.h"
#endif #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 /// 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) void try_start(void)
{ {
++trylevel; ++trylevel;
@ -50,7 +115,9 @@ void try_start(void)
/// @return true if an error occurred /// @return true if an error occurred
bool try_end(Error *err) 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 // Without this it stops processing all subsequent VimL commands and
// generates strange error messages if I e.g. try calling Test() in a // generates strange error messages if I e.g. try calling Test() in a

View File

@ -6,6 +6,7 @@
#include "nvim/api/private/defs.h" #include "nvim/api/private/defs.h"
#include "nvim/vim.h" #include "nvim/vim.h"
#include "nvim/memory.h" #include "nvim/memory.h"
#include "nvim/ex_eval.h"
#include "nvim/lib/kvec.h" #include "nvim/lib/kvec.h"
#define OBJECT_OBJ(o) o #define OBJECT_OBJ(o) o
@ -82,6 +83,21 @@
#define api_free_window(value) #define api_free_window(value)
#define api_free_tabpage(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 #ifdef INCLUDE_GENERATED_DECLARATIONS
# include "api/private/helpers.h.generated.h" # include "api/private/helpers.h.generated.h"
#endif #endif

View File

@ -11031,6 +11031,7 @@ void get_user_input(const typval_T *const argvars,
const char *defstr = ""; const char *defstr = "";
const char *cancelreturn = NULL; const char *cancelreturn = NULL;
const char *xp_name = NULL; const char *xp_name = NULL;
Callback input_callback = { .type = kCallbackNone };
char prompt_buf[NUMBUFLEN]; char prompt_buf[NUMBUFLEN];
char defstr_buf[NUMBUFLEN]; char defstr_buf[NUMBUFLEN];
char cancelreturn_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")); emsgf(_("E5050: {opts} must be the only argument"));
return; 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, ""); prompt = tv_dict_get_string_buf_chk(dict, S_LEN("prompt"), prompt_buf, "");
if (prompt == NULL) { if (prompt == NULL) {
return; return;
@ -11066,6 +11067,9 @@ void get_user_input(const typval_T *const argvars,
if (xp_name == def) { // default to NULL if (xp_name == def) { // default to NULL
xp_name = NULL; xp_name = NULL;
} }
if (!tv_dict_get_callback(dict, S_LEN("highlight"), &input_callback)) {
return;
}
} else { } else {
prompt = tv_get_string_buf_chk(&argvars[0], prompt_buf); prompt = tv_get_string_buf_chk(&argvars[0], prompt_buf);
if (prompt == NULL) { if (prompt == NULL) {
@ -11124,12 +11128,13 @@ void get_user_input(const typval_T *const argvars,
stuffReadbuffSpec(defstr); stuffReadbuffSpec(defstr);
int save_ex_normal_busy = ex_normal_busy; const int save_ex_normal_busy = ex_normal_busy;
ex_normal_busy = 0; ex_normal_busy = 0;
rettv->vval.v_string = rettv->vval.v_string =
getcmdline_prompt(inputsecret_flag ? NUL : '@', (char_u *)p, echo_attr, (char_u *)getcmdline_prompt(inputsecret_flag ? NUL : '@', p, echo_attr,
xp_type, (char_u *)xp_arg); xp_type, xp_arg, input_callback);
ex_normal_busy = save_ex_normal_busy; ex_normal_busy = save_ex_normal_busy;
callback_free(&input_callback);
if (rettv->vval.v_string == NULL && cancelreturn != NULL) { if (rettv->vval.v_string == NULL && cancelreturn != NULL) {
rettv->vval.v_string = (char_u *)xstrdup(cancelreturn); rettv->vval.v_string = (char_u *)xstrdup(cancelreturn);

View File

@ -43,7 +43,7 @@ typedef struct partial_S partial_T;
typedef struct ufunc ufunc_T; typedef struct ufunc ufunc_T;
typedef enum { typedef enum {
kCallbackNone, kCallbackNone = 0,
kCallbackFuncref, kCallbackFuncref,
kCallbackPartial, kCallbackPartial,
} CallbackType; } CallbackType;

View File

@ -189,7 +189,8 @@ void do_debug(char_u *cmd)
} }
xfree(cmdline); 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) { if (typeahead_saved) {
restore_typeahead(&typeaheadbuf); restore_typeahead(&typeaheadbuf);

View File

@ -561,7 +561,9 @@ static void discard_exception(except_T *excp, int was_finished)
*/ */
void discard_current_exception(void) 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; current_exception = NULL;
did_throw = FALSE; did_throw = FALSE;
need_rethrow = FALSE; need_rethrow = FALSE;

View File

@ -63,6 +63,9 @@
#include "nvim/os/os.h" #include "nvim/os/os.h"
#include "nvim/event/loop.h" #include "nvim/event/loop.h"
#include "nvim/os/time.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. * Variables shared between getcmdline(), redrawcmdline() and others.
@ -70,23 +73,27 @@
* structure. * structure.
*/ */
struct cmdline_info { struct cmdline_info {
char_u *cmdbuff; /* pointer to command line buffer */ char_u *cmdbuff; // pointer to command line buffer
int cmdbufflen; /* length of cmdbuff */ int cmdbufflen; // length of cmdbuff
int cmdlen; /* number of chars in command line */ int cmdlen; // number of chars in command line
int cmdpos; /* current cursor position */ int cmdpos; // current cursor position
int cmdspos; /* cursor column on screen */ int cmdspos; // cursor column on screen
int cmdfirstc; /* ':', '/', '?', '=', '>' or NUL */ int cmdfirstc; // ':', '/', '?', '=', '>' or NUL
int cmdindent; /* number of spaces before cmdline */ int cmdindent; // number of spaces before cmdline
char_u *cmdprompt; /* message in front of cmdline */ char_u *cmdprompt; // message in front of cmdline
int cmdattr; /* attributes for prompt */ int cmdattr; // attributes for prompt
int overstrike; /* Typing mode on the command line. Shared by int overstrike; // Typing mode on the command line. Shared by
getcmdline() and put_on_cmdline(). */ // getcmdline() and put_on_cmdline().
expand_T *xpc; /* struct being used for expansion, xp_pattern expand_T *xpc; // struct being used for expansion, xp_pattern
may point into cmdbuff */ // may point into cmdbuff
int xp_context; /* type of expansion */ int xp_context; // type of expansion
char_u *xp_arg; /* user-defined expansion arg */ char_u *xp_arg; // user-defined expansion arg
int input_fn; /* when TRUE Invoked for input() function */ 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 { typedef struct command_line_state {
VimState state; VimState state;
@ -136,6 +143,38 @@ typedef struct command_line_state {
struct cmdline_info save_ccline; struct cmdline_info save_ccline;
} CommandLineState; } 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 /* The current cmdline_info. It is initialized in getcmdline() and after that
* used by other functions. When invoking getcmdline() recursively it needs * used by other functions. When invoking getcmdline() recursively it needs
* to be saved with save_cmdline() and restored with restore_cmdline(). * 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 */ /* identifying (unique) number of newest history entry */
static int hislen = 0; /* actual length of history tables */ 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 #ifdef INCLUDE_GENERATED_DECLARATIONS
# include "ex_getln.c.generated.h" # 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; cmd_hkmap = 0;
} }
ccline.prompt_id = last_prompt_id++;
ccline.overstrike = false; // always start in insert mode ccline.overstrike = false; // always start in insert mode
clearpos(&s->match_end); clearpos(&s->match_end);
s->save_cursor = curwin->w_cursor; // may be restored later 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 ESC: // get here if p_wc != ESC or when ESC typed twice
case Ctrl_C: case Ctrl_C:
// In exmode it doesn't make sense to return. Except when // In exmode it doesn't make sense to return. Except when
// ":normal" runs out of characters. // ":normal" runs out of characters. Also when highlight callback is active
if (exmode_active && (ex_normal_busy == 0 || typebuf.tb_len > 0)) { // <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); return command_line_not_changed(s);
} }
@ -1790,41 +1839,50 @@ getcmdline (
return command_line_enter(firstc, count, indent); return command_line_enter(firstc, count, indent);
} }
/* /// Get a command line with a prompt
* Get a command line with a prompt. ///
* This is prepared to be called recursively from getcmdline() (e.g. by /// This is prepared to be called recursively from getcmdline() (e.g. by
* f_input() when evaluating an expression from CTRL-R =). /// f_input() when evaluating an expression from `<C-r>=`).
* Returns the command line in allocated memory, or NULL. ///
*/ /// @param[in] firstc Prompt type: e.g. '@' for input(), '>' for debug.
char_u * /// @param[in] prompt Prompt string: what is displayed before the user text.
getcmdline_prompt ( /// @param[in] attr Prompt highlighting.
int firstc, /// @param[in] xp_context Type of expansion.
char_u *prompt, /* command line prompt */ /// @param[in] xp_arg User-defined expansion argument.
int attr, /* attributes for prompt */ /// @param[in] highlight_callback Callback used for highlighting user input.
int xp_context, /* type of expansion */ ///
char_u *xp_arg /* user-defined expansion argument */ /// @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; const int msg_col_save = msg_col;
struct cmdline_info save_ccline;
int msg_col_save = msg_col;
struct cmdline_info save_ccline;
save_cmdline(&save_ccline); save_cmdline(&save_ccline);
ccline.cmdprompt = prompt;
ccline.prompt_id = last_prompt_id++;
ccline.cmdprompt = (char_u *)prompt;
ccline.cmdattr = attr; ccline.cmdattr = attr;
ccline.xp_context = xp_context; ccline.xp_context = xp_context;
ccline.xp_arg = xp_arg; ccline.xp_arg = (char_u *)xp_arg;
ccline.input_fn = (firstc == '@'); ccline.input_fn = (firstc == '@');
s = getcmdline(firstc, 1L, 0); ccline.highlight_callback = highlight_callback;
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)
msg_col = msg_col_save;
return s; 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) {
msg_col = msg_col_save;
}
return ret;
} }
/* /*
@ -2285,75 +2343,329 @@ void free_cmdline_buf(void)
# endif # 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 * Draw part of the cmdline at the current cursor position. But draw stars
* when cmdline_star is TRUE. * when cmdline_star is TRUE.
*/ */
static void draw_cmdline(int start, int len) static void draw_cmdline(int start, int len)
{ {
int i; if (!color_cmdline(&ccline, &last_ccline_colors)) {
return;
}
if (cmdline_star > 0) if (cmdline_star > 0) {
for (i = 0; i < len; ++i) { for (int i = 0; i < len; i++) {
msg_putchar('*'); msg_putchar('*');
if (has_mbyte) if (has_mbyte) {
i += (*mb_ptr2len)(ccline.cmdbuff + start + i) - 1; i += (*mb_ptr2len)(ccline.cmdbuff + start + i) - 1;
}
} }
else if (p_arshape && !p_tbidi && enc_utf8 && len > 0) { } else if (p_arshape && !p_tbidi && enc_utf8 && len > 0) {
static int buflen = 0; bool do_arabicshape = false;
char_u *p;
int j;
int newlen = 0;
int mb_l; int mb_l;
int pc, pc1 = 0; for (int i = start; i < start + len; i += mb_l) {
int prev_c = 0; char_u *p = ccline.cmdbuff + i;
int prev_c1 = 0; int u8cc[MAX_MCO];
int u8c; int u8c = utfc_ptr2char_len(p, u8cc, start + len - i);
int u8cc[MAX_MCO]; mb_l = utfc_ptr2len_len(p, start + len - i);
int nc = 0; if (arabic_char(u8c)) {
do_arabicshape = true;
break;
}
}
if (!do_arabicshape) {
goto draw_cmdline_no_arabicshape;
}
/* static int buflen = 0;
* Do arabic shaping into a temporary buffer. This is very
* inefficient! // Do arabic shaping into a temporary buffer. This is very
*/ // inefficient!
if (len * 2 + 2 > buflen) { if (len * 2 + 2 > buflen) {
/* Re-allocate the buffer. We keep it around to avoid a lot of // Re-allocate the buffer. We keep it around to avoid a lot of
* alloc()/free() calls. */ // alloc()/free() calls.
xfree(arshape_buf); xfree(arshape_buf);
buflen = len * 2 + 2; buflen = len * 2 + 2;
arshape_buf = xmalloc(buflen); arshape_buf = xmalloc(buflen);
} }
int newlen = 0;
if (utf_iscomposing(utf_ptr2char(ccline.cmdbuff + start))) { 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] = ' '; arshape_buf[0] = ' ';
newlen = 1; newlen = 1;
} }
for (j = start; j < start + len; j += mb_l) { int prev_c = 0;
p = ccline.cmdbuff + j; int prev_c1 = 0;
u8c = utfc_ptr2char_len(p, u8cc, start + len - j); for (int i = start; i < start + len; i += mb_l) {
mb_l = utfc_ptr2len_len(p, start + len - j); 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)) { if (arabic_char(u8c)) {
/* Do Arabic shaping. */ int pc;
int pc1 = 0;
int nc = 0;
// Do Arabic shaping.
if (cmdmsg_rl) { if (cmdmsg_rl) {
/* displaying from right to left */ // Displaying from right to left.
pc = prev_c; pc = prev_c;
pc1 = prev_c1; pc1 = prev_c1;
prev_c1 = u8cc[0]; prev_c1 = u8cc[0];
if (j + mb_l >= start + len) if (i + mb_l >= start + len) {
nc = NUL; nc = NUL;
else } else {
nc = utf_ptr2char(p + mb_l); nc = utf_ptr2char(p + mb_l);
}
} else { } else {
/* displaying from left to right */ // Displaying from left to right.
if (j + mb_l >= start + len) if (i + mb_l >= start + len) {
pc = NUL; pc = NUL;
else { } else {
int pcc[MAX_MCO]; int pcc[MAX_MCO];
pc = utfc_ptr2char_len(p + mb_l, pcc, pc = utfc_ptr2char_len(p + mb_l, pcc, start + len - i - mb_l);
start + len - j - mb_l);
pc1 = pcc[0]; pc1 = pcc[0];
} }
nc = prev_c; nc = prev_c;
@ -2377,8 +2689,23 @@ static void draw_cmdline(int start, int len)
} }
msg_outtrans_len(arshape_buf, newlen); msg_outtrans_len(arshape_buf, newlen);
} else } else {
msg_outtrans_len(ccline.cmdbuff + start, len); 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);
}
}
} }
/* /*

View File

@ -75,7 +75,7 @@ struct interval {
/* /*
* Like utf8len_tab above, but using a zero for illegal lead bytes. * 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,
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,

View File

@ -1,6 +1,7 @@
#ifndef NVIM_MBYTE_H #ifndef NVIM_MBYTE_H
#define NVIM_MBYTE_H #define NVIM_MBYTE_H
#include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <string.h> #include <string.h>
@ -67,6 +68,8 @@ typedef struct {
///< otherwise use '?'. ///< otherwise use '?'.
} vimconv_T; } vimconv_T;
extern const uint8_t utf8len_tab_zero[256];
#ifdef INCLUDE_GENERATED_DECLARATIONS #ifdef INCLUDE_GENERATED_DECLARATIONS
# include "mbyte.h.generated.h" # include "mbyte.h.generated.h"
#endif #endif

View File

@ -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(). * The display part of msg_puts_attr_len().
* May be called recursively to display scroll-back text. * May be called recursively to display scroll-back text.

View File

@ -207,7 +207,7 @@ nolog:
# New style of tests uses Vim script with assert calls. These are easier # New style of tests uses Vim script with assert calls. These are easier
# to write and a lot easier to read and debug. # to write and a lot easier to read and debug.
# Limitation: Only works with the +eval feature. # 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 newtests: newtestssilent
@/bin/sh -c "if test -f messages && grep -q 'FAILED' messages; then \ @/bin/sh -c "if test -f messages && grep -q 'FAILED' messages; then \

View File

@ -23,10 +23,41 @@ before_each(function()
function CustomListCompl(...) function CustomListCompl(...)
return ['FOO'] return ['FOO']
endfunction 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({ screen:set_default_attr_ids({
EOB={bold = true, foreground = Screen.colors.Blue1}, EOB={bold = true, foreground = Screen.colors.Blue1},
T={foreground=Screen.colors.Red}, 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) end)
@ -196,6 +227,18 @@ describe('input()', function()
eq('Vim(call):E118: Too many arguments for function: input', eq('Vim(call):E118: Too many arguments for function: input',
exc_exec('call input("prompt> ", "default", "file", "extra")')) exc_exec('call input("prompt> ", "default", "file", "extra")'))
end) 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) end)
describe('inputdialog()', function() describe('inputdialog()', function()
it('works with multiline prompts', function() it('works with multiline prompts', function()
@ -363,4 +406,16 @@ describe('inputdialog()', function()
eq('Vim(call):E118: Too many arguments for function: inputdialog', eq('Vim(call):E118: Too many arguments for function: inputdialog',
exc_exec('call inputdialog("prompt> ", "default", "file", "extra")')) exc_exec('call inputdialog("prompt> ", "default", "file", "extra")'))
end) 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) end)

View File

@ -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)

View File

@ -251,7 +251,7 @@ function Screen:expect(expected, attr_ids, attr_ignore, condition, any)
..'Expected:\n |'..table.concat(msg_expected_rows, '|\n |')..'|\n' ..'Expected:\n |'..table.concat(msg_expected_rows, '|\n |')..'|\n'
..'Actual:\n |'..table.concat(actual_rows, '|\n |')..'|\n\n'..[[ ..'Actual:\n |'..table.concat(actual_rows, '|\n |')..'|\n\n'..[[
To print the expect() call that would assert the current screen state, use 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. ]]) screen:redraw_debug() to show all intermediate screen states. ]])
end end
end end