diff options
-rw-r--r-- | runtime/doc/ui.txt | 68 | ||||
-rw-r--r-- | src/nvim/api/private/helpers.c | 18 | ||||
-rw-r--r-- | src/nvim/api/ui.c | 7 | ||||
-rw-r--r-- | src/nvim/api/ui_events.in.h | 13 | ||||
-rw-r--r-- | src/nvim/eval.c | 7 | ||||
-rw-r--r-- | src/nvim/ex_getln.c | 11 | ||||
-rw-r--r-- | src/nvim/garray.h | 1 | ||||
-rw-r--r-- | src/nvim/message.c | 196 | ||||
-rw-r--r-- | src/nvim/message.h | 4 | ||||
-rw-r--r-- | src/nvim/normal.c | 35 | ||||
-rw-r--r-- | src/nvim/ops.c | 8 | ||||
-rw-r--r-- | src/nvim/option.c | 6 | ||||
-rw-r--r-- | src/nvim/quickfix.c | 2 | ||||
-rw-r--r-- | src/nvim/screen.c | 94 | ||||
-rw-r--r-- | src/nvim/ui.c | 6 | ||||
-rw-r--r-- | src/nvim/ui.h | 4 | ||||
-rw-r--r-- | test/functional/api/vim_spec.lua | 5 | ||||
-rw-r--r-- | test/functional/ui/messages_spec.lua | 632 | ||||
-rw-r--r-- | test/functional/ui/options_spec.lua | 1 | ||||
-rw-r--r-- | test/functional/ui/screen.lua | 55 |
20 files changed, 1109 insertions, 64 deletions
diff --git a/runtime/doc/ui.txt b/runtime/doc/ui.txt index c6a06a4531..84a24ef2a1 100644 --- a/runtime/doc/ui.txt +++ b/runtime/doc/ui.txt @@ -32,6 +32,7 @@ a dictionary with these (optional) keys: `ext_tabline` Externalize the tabline. |ui-tabline| `ext_cmdline` Externalize the cmdline. |ui-cmdline| `ext_wildmenu` Externalize the wildmenu. |ui-wildmenu| + `ext_messages` Externalize messages. |ui-messages| `ext_linegrid` Use new revision of the grid events. |ui-linegrid| `ext_multigrid` Use per-window grid based events. |ui-multigrid| `ext_hlstate` Use detailed highlight state. |ui-hlstate| @@ -572,7 +573,7 @@ Only sent if `ext_tabline` option is set in |ui-options| ============================================================================== Cmdline Events *ui-cmdline* -Only sent if `ext_cmdline` option is set in |ui-options| +Only sent if `ext_cmdline` option is set in |ui-options|. ["cmdline_show", content, pos, firstc, prompt, indent, level] content: List of [attrs, string] @@ -645,4 +646,69 @@ Only sent if `ext_wildmenu` option is set in |ui-options| Hide the wildmenu. ============================================================================== +Message Events *ui-messages* + +Only sent if `ext_messages` option is set in |ui-options|. This option implies +`ext_linegrid` and `ext_cmdline` also being set. |ui-linegrid| and |ui-cmdline| events +will thus also be sent. + +This extension allows the UI to control the display of messages that otherwise +would have been displayed in the message/cmdline area in the bottom of the +screen. + +Activating this extension means that Nvim will allocate no screen space for +the cmdline or messages, and 'cmdheight' will be set to zero. Attempting to +change 'cmdheight' will silently be ignored. |ui-cmdline| events will be used +to represent the state of the cmdline. + +["msg_show", kind, content, replace_last] + Display a message to the user. + + `kind` will be one of the following + `emsg`: Internal error message + `echo`: temporary message from plugin (|:echo|) + `echomsg`: ordinary message from plugin (|:echomsg|) + `echoerr`: error message from plugin (|:echoerr|) + `return_prompt`: |press-enter| prompt after a group of messages + `quickfix`: Quickfix navigation message + `kind` can also be the empty string. The message is then some internal + informative or warning message, that hasn't yet been assigned a kind. + New message kinds can be added in later versions; clients should + handle messages of an unknown kind just like empty kind. + + `content` will be an array of `[attr_id, text_chunk]` tuples, + building up the message text of chunks of different highlights. + No extra spacing should be added between chunks, the `text_chunk` by + itself should contain any necessary whitespace. Messages can contain + line breaks `"\n"`. + + `replace_last` controls how multiple messages should be displayed. + If `replace_last` is false, this message should be displayed together + with all previous messages that are still visible. If `replace_last` + is true, this message should replace the message in the most recent + `msg_show` call, but any other visible message should still remain. + +["msg_clear"] + Clear all messages currently displayed by "msg_show". (Messages sent + by other "msg_" events below will not be affected). + +["msg_showmode", content] + Shows 'showmode' and |recording| messages. `content` has the same + format as in "msg_show". This event is sent with empty `content` to + hide the last message. + +["msg_showcmd", content] + Shows 'showcmd' messages. `content` has the same format as in "msg_show". + This event is sent with empty `content` to hide the last message. + +["msg_ruler", content] + Used to display 'ruler' when there is no space for the ruler in a + statusline. `content` has the same format as in "msg_show". This event is + sent with empty `content` to hide the last message. + +["msg_history_show", entries] + Sent when |:messages| command is invoked. History is sent as a list of + entries, where each entry is a `[kind, content]` tuple. + +============================================================================== vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index f5cac82315..19a3368c1c 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -712,6 +712,12 @@ String cbuf_to_string(const char *buf, size_t size) }; } +String cstrn_to_string(const char *str, size_t maxsize) + FUNC_ATTR_NONNULL_ALL +{ + return cbuf_to_string(str, strnlen(str, maxsize)); +} + /// Creates a String using the given C string. Unlike /// cstr_to_string this function DOES NOT copy the C string. /// @@ -726,6 +732,18 @@ String cstr_as_string(char *str) FUNC_ATTR_PURE return (String){ .data = str, .size = strlen(str) }; } +/// Return the owned memory of a ga as a String +/// +/// Reinitializes the ga to a valid empty state. +String ga_take_string(garray_T *ga) +{ + String str = { .data = (char *)ga->ga_data, .size = (size_t)ga->ga_len }; + ga->ga_data = NULL; + ga->ga_len = 0; + ga->ga_maxlen = 0; + return str; +} + /// Collects `n` buffer lines into array `l`, optionally replacing newlines /// with NUL. /// diff --git a/src/nvim/api/ui.c b/src/nvim/api/ui.c index 91009c950f..9e9be588e3 100644 --- a/src/nvim/api/ui.c +++ b/src/nvim/api/ui.c @@ -132,6 +132,13 @@ void nvim_ui_attach(uint64_t channel_id, Integer width, Integer height, ui->ui_ext[kUILinegrid] = true; } + if (ui->ui_ext[kUIMessages]) { + // This uses attribute indicies, so ext_linegrid is needed. + ui->ui_ext[kUILinegrid] = true; + // Cmdline uses the messages area, so it should be externalized too. + ui->ui_ext[kUICmdline] = true; + } + UIData *data = xmalloc(sizeof(UIData)); data->channel_id = channel_id; data->buffer = (Array)ARRAY_DICT_INIT; diff --git a/src/nvim/api/ui_events.in.h b/src/nvim/api/ui_events.in.h index ef3ff0f4c2..b57cf8d3ef 100644 --- a/src/nvim/api/ui_events.in.h +++ b/src/nvim/api/ui_events.in.h @@ -141,4 +141,17 @@ void wildmenu_select(Integer selected) FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY; void wildmenu_hide(void) FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY; + +void msg_show(String kind, Array content, Boolean replace_last) + FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; +void msg_clear(void) + FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; +void msg_showcmd(Array content) + FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; +void msg_showmode(Array content) + FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; +void msg_ruler(Array content) + FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; +void msg_history_show(Array entries) + FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; #endif // NVIM_API_UI_EVENTS_IN_H diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 4ab699cdb7..d63e45d3c7 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -19597,7 +19597,10 @@ void ex_echo(exarg_T *eap) msg_puts_attr(" ", echo_attr); } char *tofree = encode_tv2echo(&rettv, NULL); - msg_multiline_attr(tofree, echo_attr); + if (*tofree != NUL) { + msg_ext_set_kind("echo"); + msg_multiline_attr(tofree, echo_attr); + } xfree(tofree); } tv_clear(&rettv); @@ -19689,11 +19692,13 @@ void ex_execute(exarg_T *eap) } if (eap->cmdidx == CMD_echomsg) { + msg_ext_set_kind("echomsg"); MSG_ATTR(ga.ga_data, echo_attr); ui_flush(); } else if (eap->cmdidx == CMD_echoerr) { /* We don't want to abort following commands, restore did_emsg. */ save_did_emsg = did_emsg; + msg_ext_set_kind("echoerr"); EMSG((char_u *)ga.ga_data); if (!force_abort) did_emsg = save_did_emsg; diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index e8375cd3cc..d70b81409d 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -308,6 +308,9 @@ static uint8_t *command_line_enter(int firstc, long count, int indent) gotocmdline(true); redrawcmdprompt(); // draw prompt or indent set_cmdspos(); + if (!msg_scroll) { + msg_ext_clear(false); + } } s->xpc.xp_context = EXPAND_NOTHING; s->xpc.xp_backslash = XP_BS_NONE; @@ -496,6 +499,12 @@ static uint8_t *command_line_enter(int firstc, long count, int indent) if (ui_has(kUICmdline)) { ui_call_cmdline_hide(ccline.level); + if (msg_ext_is_visible()) { + msg_ext_did_cmdline = true; + if (must_redraw < VALID) { + must_redraw = VALID; + } + } } cmdline_level--; @@ -3613,7 +3622,7 @@ nextwild ( return FAIL; } - if (!ui_has(kUIWildmenu)) { + if (!(ui_has(kUICmdline) || ui_has(kUIWildmenu))) { MSG_PUTS("..."); // show that we are busy ui_flush(); } diff --git a/src/nvim/garray.h b/src/nvim/garray.h index 58738df691..e2cbdd4eab 100644 --- a/src/nvim/garray.h +++ b/src/nvim/garray.h @@ -18,6 +18,7 @@ typedef struct growarray { } garray_T; #define GA_EMPTY_INIT_VALUE { 0, 0, 0, 1, NULL } +#define GA_INIT(itemsize, growsize) { 0, 0, (itemsize), (growsize), NULL } #define GA_EMPTY(ga_ptr) ((ga_ptr)->ga_len <= 0) diff --git a/src/nvim/message.c b/src/nvim/message.c index 971a14974c..b22508c23f 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -34,11 +34,13 @@ #include "nvim/regexp.h" #include "nvim/screen.h" #include "nvim/strings.h" +#include "nvim/syntax.h" #include "nvim/ui.h" #include "nvim/mouse.h" #include "nvim/os/os.h" #include "nvim/os/input.h" #include "nvim/os/time.h" +#include "nvim/api/private/helpers.h" /* * To be able to scroll back at the "more" and "hit-enter" prompts we need to @@ -108,6 +110,19 @@ static int verbose_did_open = FALSE; * This is an allocated string or NULL when not used. */ + +// Extended msg state, currently used for external UIs with ext_messages +static const char *msg_ext_kind = NULL; +static Array msg_ext_chunks = ARRAY_DICT_INIT; +static garray_T msg_ext_last_chunk = GA_INIT(sizeof(char), 40); +static sattr_T msg_ext_last_attr = -1; + +static bool msg_ext_overwrite = false; ///< will overwrite last message +static int msg_ext_visible = 0; ///< number of messages currently visible + +/// Shouldn't clear message after leaving cmdline +static bool msg_ext_keep_after_cmdline = false; + /* * msg(s) - displays the string 's' on the status line * When terminal not initialized (yet) mch_errmsg(..) is used. @@ -256,7 +271,8 @@ msg_strtrunc ( /* May truncate message to avoid a hit-return prompt */ if ((!msg_scroll && !need_wait_return && shortmess(SHM_TRUNCALL) - && !exmode_active && msg_silent == 0) || force) { + && !exmode_active && msg_silent == 0 && !ui_has(kUIMessages)) + || force) { len = vim_strsize(s); if (msg_scrolled != 0) /* Use all the columns. */ @@ -594,6 +610,9 @@ static bool emsg_multiline(const char *s, bool multiline) } // wait_return has reset need_wait_return // and a redraw is expected because // msg_scrolled is non-zero + if (msg_ext_kind == NULL) { + msg_ext_set_kind("emsg"); + } /* * Display name and line number for the source of the error. @@ -802,6 +821,7 @@ static void add_msg_hist(const char *s, int len, int attr, bool multiline) p->next = NULL; p->attr = attr; p->multiline = multiline; + p->kind = msg_ext_kind; if (last_msg_hist != NULL) { last_msg_hist->next = p; } @@ -856,7 +876,6 @@ void ex_messages(void *const eap_p) return; } - msg_hist_off = true; p = first_msg_hist; @@ -874,13 +893,31 @@ void ex_messages(void *const eap_p) } // Display what was not skipped. - for (; p != NULL && !got_int; p = p->next) { - if (p->msg != NULL) { - msg_attr_keep(p->msg, p->attr, false, p->multiline); + if (ui_has(kUIMessages)) { + Array entries = ARRAY_DICT_INIT; + for (; p != NULL; p = p->next) { + if (p->msg != NULL && p->msg[0] != NUL) { + Array entry = ARRAY_DICT_INIT; + ADD(entry, STRING_OBJ(cstr_to_string(p->kind))); + Array content_entry = ARRAY_DICT_INIT; + ADD(content_entry, INTEGER_OBJ(p->attr)); + ADD(content_entry, STRING_OBJ(cstr_to_string((char *)(p->msg)))); + Array content = ARRAY_DICT_INIT; + ADD(content, ARRAY_OBJ(content_entry)); + ADD(entry, ARRAY_OBJ(content)); + ADD(entries, ARRAY_OBJ(entry)); + } + } + ui_call_msg_history_show(entries); + } else { + msg_hist_off = true; + for (; p != NULL && !got_int; p = p->next) { + if (p->msg != NULL) { + msg_attr_keep(p->msg, p->attr, false, p->multiline); + } } + msg_hist_off = false; } - - msg_hist_off = false; } /* @@ -1058,8 +1095,9 @@ void wait_return(int redraw) if (c == ':' || c == '?' || c == '/') { if (!exmode_active) cmdline_row = msg_row; - skip_redraw = TRUE; /* skip redraw once */ - do_redraw = FALSE; + skip_redraw = true; // skip redraw once + do_redraw = false; + msg_ext_keep_after_cmdline = true; } /* @@ -1084,9 +1122,13 @@ void wait_return(int redraw) if (tmpState == SETWSIZE) { /* got resize event while in vgetc() */ ui_refresh(); - } else if (!skip_redraw - && (redraw == TRUE || (msg_scrolled != 0 && redraw != -1))) { - redraw_later(VALID); + } else if (!skip_redraw) { + if (redraw == true || (msg_scrolled != 0 && redraw != -1)) { + redraw_later(VALID); + } + if (ui_has(kUIMessages)) { + msg_ext_clear(true); + } } } @@ -1100,8 +1142,10 @@ static void hit_return_msg(void) p_more = FALSE; /* don't want see this message when scrolling back */ if (msg_didout) /* start on a new line */ msg_putchar('\n'); - if (got_int) + msg_ext_set_kind("return_prompt"); + if (got_int) { MSG_PUTS(_("Interrupt: ")); + } MSG_PUTS_ATTR(_("Press ENTER or type command to continue"), HL_ATTR(HLF_R)); if (!msg_use_printf()) { @@ -1124,6 +1168,17 @@ void set_keep_msg(char_u *s, int attr) keep_msg_attr = attr; } +void msg_ext_set_kind(const char *msg_kind) +{ + // Don't change the label of an existing batch: + msg_ext_ui_flush(); + + // TODO(bfredl): would be nice to avoid dynamic scoping, but that would + // need refactoring the msg_ interface to not be "please pretend nvim is + // a terminal for a moment" + msg_ext_kind = msg_kind; +} + /* * Prepare for outputting characters in the command line. */ @@ -1160,6 +1215,14 @@ void msg_start(void) msg_didout = FALSE; /* no output on current line yet */ } + if (ui_has(kUIMessages)) { + msg_ext_ui_flush(); + if (!msg_scroll && msg_ext_visible) { + // Will overwrite last message. + msg_ext_overwrite = true; + } + } + // When redirecting, may need to start a new line. if (!did_return) { redir_write("\n", 1); @@ -1727,7 +1790,18 @@ void msg_puts_attr_len(const char *const str, const ptrdiff_t len, int attr) // wait-return prompt later. Needed when scrolling, resetting // need_wait_return after some prompt, and then outputting something // without scrolling - if (msg_scrolled != 0 && !msg_scrolled_ign) { + bool overflow = false; + if (ui_has(kUIMessages)) { + int count = msg_ext_visible + (msg_ext_overwrite ? 0 : 1); + // TODO(bfredl): possible extension point, let external UI control this + if (count > 1) { + overflow = true; + } + } else { + overflow = msg_scrolled != 0; + } + + if (overflow && !msg_scrolled_ign) { need_wait_return = true; } msg_didany = true; // remember that something was outputted @@ -1765,6 +1839,20 @@ void msg_printf_attr(const int attr, const char *const fmt, ...) msg_puts_attr_len(msgbuf, (ptrdiff_t)len, attr); } +static void msg_ext_emit_chunk(void) +{ + // Color was changed or a message flushed, end current chunk. + if (msg_ext_last_attr == -1) { + return; // no chunk + } + Array chunk = ARRAY_DICT_INIT; + ADD(chunk, INTEGER_OBJ(msg_ext_last_attr)); + msg_ext_last_attr = -1; + String text = ga_take_string(&msg_ext_last_chunk); + ADD(chunk, STRING_OBJ(text)); + ADD(msg_ext_chunks, ARRAY_OBJ(chunk)); +} + /* * The display part of msg_puts_attr_len(). * May be called recursively to display scroll-back text. @@ -1783,6 +1871,18 @@ static void msg_puts_display(const char_u *str, int maxlen, int attr, int did_last_char; did_wait_return = false; + + if (ui_has(kUIMessages)) { + if (attr != msg_ext_last_attr) { + msg_ext_emit_chunk(); + msg_ext_last_attr = attr; + } + // Concat pieces with the same highlight + ga_concat_len(&msg_ext_last_chunk, (char *)str, + strnlen((char *)str, maxlen)); + return; + } + while ((maxlen < 0 || (int)(s - str) < maxlen) && *s != NUL) { // We are at the end of the screen line when: // - When outputting a newline. @@ -2607,6 +2707,9 @@ void msg_clr_eos(void) */ void msg_clr_eos_force(void) { + if (ui_has(kUIMessages)) { + return; + } int msg_startcol = (cmdmsg_rl) ? 0 : msg_col; int msg_endcol = (cmdmsg_rl) ? msg_col + 1 : (int)Columns; @@ -2643,8 +2746,66 @@ int msg_end(void) wait_return(FALSE); return FALSE; } - ui_flush(); - return TRUE; + + // @TODO(bfredl): calling flush here inhibits substantial performance + // improvements. Caller should call ui_flush before waiting on user input or + // CPU busywork. + ui_flush(); // calls msg_ext_ui_flush + return true; +} + +void msg_ext_ui_flush(void) +{ + if (!ui_has(kUIMessages)) { + return; + } + + msg_ext_emit_chunk(); + if (msg_ext_chunks.size > 0) { + ui_call_msg_show(cstr_to_string(msg_ext_kind), + msg_ext_chunks, msg_ext_overwrite); + if (!msg_ext_overwrite) { + msg_ext_visible++; + } + msg_ext_kind = NULL; + msg_ext_chunks = (Array)ARRAY_DICT_INIT; + msg_ext_overwrite = false; + } +} + +void msg_ext_flush_showmode(void) +{ + // Showmode messages doesn't interrupt normal message flow, so we use + // separate event. Still reuse the same chunking logic, for simplicity. + msg_ext_emit_chunk(); + ui_call_msg_showmode(msg_ext_chunks); + msg_ext_chunks = (Array)ARRAY_DICT_INIT; +} + +void msg_ext_clear(bool force) +{ + if (msg_ext_visible && (!msg_ext_keep_after_cmdline || force)) { + ui_call_msg_clear(); + msg_ext_visible = 0; + msg_ext_overwrite = false; // nothing to overwrite + } + + // Only keep once. + msg_ext_keep_after_cmdline = false; +} + +void msg_ext_check_prompt(void) +{ + // Redraw after cmdline is expected to clear messages. + if (msg_ext_did_cmdline) { + msg_ext_clear(true); + msg_ext_did_cmdline = false; + } +} + +bool msg_ext_is_visible(void) +{ + return ui_has(kUIMessages) && msg_ext_visible > 0; } /* @@ -2653,6 +2814,9 @@ int msg_end(void) */ void msg_check(void) { + if (ui_has(kUIMessages)) { + return; + } if (msg_row == Rows - 1 && msg_col >= sc_col) { need_wait_return = TRUE; redraw_cmdline = TRUE; diff --git a/src/nvim/message.h b/src/nvim/message.h index 41d2945b9c..7938fd91d3 100644 --- a/src/nvim/message.h +++ b/src/nvim/message.h @@ -5,6 +5,7 @@ #include <stdarg.h> #include <stddef.h> +#include "nvim/macros.h" #include "nvim/types.h" /* @@ -77,6 +78,7 @@ typedef struct msg_hist { struct msg_hist *next; ///< Next message. char_u *msg; ///< Message text. + const char *kind; ///< Message kind (for msg_ext) int attr; ///< Message highlighting. bool multiline; ///< Multiline message. } MessageHistoryEntry; @@ -86,6 +88,8 @@ extern MessageHistoryEntry *first_msg_hist; /// Last message extern MessageHistoryEntry *last_msg_hist; +EXTERN bool msg_ext_did_cmdline INIT(= false); + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "message.h.generated.h" #endif diff --git a/src/nvim/normal.c b/src/nvim/normal.c index 44e6ab46f1..d361d81ac7 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -61,6 +61,7 @@ #include "nvim/event/loop.h" #include "nvim/os/time.h" #include "nvim/os/input.h" +#include "nvim/api/private/helpers.h" typedef struct normal_state { VimState state; @@ -1258,8 +1259,9 @@ static void normal_redraw(NormalState *s) maketitle(); } - // display message after redraw - if (keep_msg != NULL) { + // Display message after redraw. If an external message is still visible, + // it contains the kept message already. + if (keep_msg != NULL && !msg_ext_is_visible()) { // msg_attr_keep() will set keep_msg to NULL, must free the string here. // Don't reset keep_msg, msg_attr_keep() uses it to check for duplicates. char *p = (char *)keep_msg; @@ -3317,7 +3319,8 @@ void clear_showcmd(void) else sprintf((char *)showcmd_buf, "%d-%d", chars, bytes); } - showcmd_buf[SHOWCMD_COLS] = NUL; /* truncate */ + int limit = ui_has(kUIMessages) ? SHOWCMD_BUFLEN-1 : SHOWCMD_COLS; + showcmd_buf[limit] = NUL; // truncate showcmd_visual = true; } else { showcmd_buf[0] = NUL; @@ -3370,8 +3373,9 @@ bool add_to_showcmd(int c) STRCPY(p, "<20>"); size_t old_len = STRLEN(showcmd_buf); size_t extra_len = STRLEN(p); - if (old_len + extra_len > SHOWCMD_COLS) { - size_t overflow = old_len + extra_len - SHOWCMD_COLS; + size_t limit = ui_has(kUIMessages) ? SHOWCMD_BUFLEN-1 : SHOWCMD_COLS; + if (old_len + extra_len > limit) { + size_t overflow = old_len + extra_len - limit; memmove(showcmd_buf, showcmd_buf + overflow, old_len - overflow + 1); } STRCAT(showcmd_buf, p); @@ -3432,13 +3436,24 @@ void pop_showcmd(void) static void display_showcmd(void) { int len; - len = (int)STRLEN(showcmd_buf); - if (len == 0) { - showcmd_is_clear = true; - } else { + showcmd_is_clear = (len == 0); + + if (ui_has(kUIMessages)) { + Array content = ARRAY_DICT_INIT; + if (len > 0) { + Array chunk = ARRAY_DICT_INIT; + // placeholder for future highlight support + ADD(chunk, INTEGER_OBJ(0)); + ADD(chunk, STRING_OBJ(cstr_to_string((char *)showcmd_buf))); + ADD(content, ARRAY_OBJ(chunk)); + } + ui_call_msg_showcmd(content); + return; + } + + if (!showcmd_is_clear) { grid_puts(&default_grid, showcmd_buf, (int)Rows - 1, sc_col, 0); - showcmd_is_clear = false; } /* diff --git a/src/nvim/ops.c b/src/nvim/ops.c index e9cb480647..674a9244f0 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -865,8 +865,12 @@ int do_record(int c) * needs to be removed again to put it in a register. exec_reg then * adds the escaping back later. */ - Recording = FALSE; - MSG(""); + Recording = false; + if (ui_has(kUIMessages)) { + showmode(); + } else { + MSG(""); + } p = get_recorded(); if (p == NULL) retval = FAIL; diff --git a/src/nvim/option.c b/src/nvim/option.c index 5517768194..fc1fab834e 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -4168,7 +4168,8 @@ static char *set_num_option(int opt_idx, char_u *varp, long value, errmsg = e_positive; } } else if (pp == &p_ch) { - if (value < 1) { + int minval = ui_has(kUIMessages) ? 0 : 1; + if (value < minval) { errmsg = e_positive; } } else if (pp == &p_tm) { @@ -4276,6 +4277,9 @@ static char *set_num_option(int opt_idx, char_u *varp, long value, p_window = Rows - 1; } } else if (pp == &p_ch) { + if (ui_has(kUIMessages)) { + p_ch = 0; + } if (p_ch > Rows - min_rows() + 1) { p_ch = Rows - min_rows() + 1; } diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c index 200995bbeb..f0c37c0e38 100644 --- a/src/nvim/quickfix.c +++ b/src/nvim/quickfix.c @@ -44,6 +44,7 @@ #include "nvim/window.h" #include "nvim/os/os.h" #include "nvim/os/input.h" +#include "nvim/api/private/helpers.h" struct dir_stack_T { @@ -2155,6 +2156,7 @@ win_found: } else if (!msg_scrolled && shortmess(SHM_OVERALL)) { msg_scroll = false; } + msg_ext_set_kind("quickfix"); msg_attr_keep(IObuff, 0, true, false); msg_scroll = (int)i; } diff --git a/src/nvim/screen.c b/src/nvim/screen.c index aa2982ec0c..5ac90ab601 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -362,6 +362,8 @@ void update_screen(int type) need_wait_return = FALSE; } + msg_ext_check_prompt(); + /* reset cmdline_row now (may have been changed temporarily) */ compute_cmdrow(); @@ -483,8 +485,9 @@ void update_screen(int type) /* Clear or redraw the command line. Done last, because scrolling may * mess up the command line. */ - if (clear_cmdline || redraw_cmdline) + if (clear_cmdline || redraw_cmdline) { showmode(); + } /* May put up an introductory message when not editing a file */ if (!did_intro) @@ -5864,7 +5867,8 @@ void grid_fill(ScreenGrid *grid, int start_row, int end_row, int start_col, } // TODO(bfredl): The relevant caller should do this - if (row == Rows - 1) { // overwritten the command line + if (row == Rows - 1 && !ui_has(kUIMessages)) { + // overwritten the command line redraw_cmdline = true; if (start_col == 0 && end_col == Columns && c1 == ' ' && c2 == ' ' && attr == 0) { @@ -6394,6 +6398,13 @@ int showmode(void) int nwr_save; int sub_attr; + if (ui_has(kUIMessages) && clear_cmdline) { + msg_ext_clear(true); + } + + // don't make non-flushed message part of the showmode + msg_ext_ui_flush(); + do_mode = ((p_smd && msg_silent == 0) && ((State & TERM_FOCUS) || (State & INSERT) @@ -6436,9 +6447,14 @@ int showmode(void) MSG_PUTS_ATTR("--", attr); // CTRL-X in Insert mode if (edit_submode != NULL && !shortmess(SHM_COMPLETIONMENU)) { - /* These messages can get long, avoid a wrap in a narrow - * window. Prefer showing edit_submode_extra. */ - length = (Rows - msg_row) * Columns - 3; + // These messages can get long, avoid a wrap in a narrow window. + // Prefer showing edit_submode_extra. With external messages there + // is no imposed limit. + if (ui_has(kUIMessages)) { + length = INT_MAX; + } else { + length = (Rows - msg_row) * Columns - 3; + } if (edit_submode_extra != NULL) { length -= vim_strsize(edit_submode_extra); } @@ -6540,6 +6556,9 @@ int showmode(void) msg_clr_cmdline(); } + // NB: also handles clearing the showmode if it was emtpy or disabled + msg_ext_flush_showmode(); + /* In Visual mode the size of the selected area must be redrawn. */ if (VIsual_active) clear_showcmd(); @@ -6581,11 +6600,13 @@ void unshowmode(bool force) // Clear the mode message. void clearmode(void) { - msg_pos_mode(); - if (Recording) { - recording_mode(HL_ATTR(HLF_CM)); - } - msg_clr_eos(); + msg_ext_ui_flush(); + msg_pos_mode(); + if (Recording) { + recording_mode(HL_ATTR(HLF_CM)); + } + msg_clr_eos(); + msg_ext_flush_showmode(); } static void recording_mode(int attr) @@ -6894,9 +6915,12 @@ void showruler(int always) static void win_redr_ruler(win_T *wp, int always) { - /* If 'ruler' off or redrawing disabled, don't do anything */ - if (!p_ru) + static bool did_show_ext_ruler = false; + + // If 'ruler' off or redrawing disabled, don't do anything + if (!p_ru) { return; + } /* * Check if cursor.lnum is valid, since win_redr_ruler() may be called @@ -6951,12 +6975,14 @@ static void win_redr_ruler(win_T *wp, int always) int fillchar; int attr; int off; + bool part_of_status = false; if (wp->w_status_height) { row = W_ENDROW(wp); fillchar = fillchar_status(&attr, wp); off = wp->w_wincol; width = wp->w_width; + part_of_status = true; } else { row = Rows - 1; fillchar = ' '; @@ -7016,23 +7042,39 @@ static void win_redr_ruler(win_T *wp, int always) } get_rel_pos(wp, buffer + i, RULER_BUF_LEN - i); } - // Truncate at window boundary. - o = 0; - for (i = 0; buffer[i] != NUL; i += utfc_ptr2len(buffer + i)) { - o += utf_ptr2cells(buffer + i); - if (this_ru_col + o > width) { - buffer[i] = NUL; - break; + + if (ui_has(kUIMessages) && !part_of_status) { + Array content = ARRAY_DICT_INIT; + Array chunk = ARRAY_DICT_INIT; + ADD(chunk, INTEGER_OBJ(attr)); + ADD(chunk, STRING_OBJ(cstr_to_string((char *)buffer))); + ADD(content, ARRAY_OBJ(chunk)); + ui_call_msg_ruler(content); + did_show_ext_ruler = true; + } else { + if (did_show_ext_ruler) { + ui_call_msg_ruler((Array)ARRAY_DICT_INIT); + did_show_ext_ruler = false; + } + // Truncate at window boundary. + o = 0; + for (i = 0; buffer[i] != NUL; i += utfc_ptr2len(buffer + i)) { + o += utf_ptr2cells(buffer + i); + if (this_ru_col + o > width) { + buffer[i] = NUL; + break; + } } + + grid_puts(&default_grid, buffer, row, this_ru_col + off, attr); + i = redraw_cmdline; + grid_fill(&default_grid, row, row + 1, + this_ru_col + off + (int)STRLEN(buffer), off + width, fillchar, + fillchar, attr); + // don't redraw the cmdline because of showing the ruler + redraw_cmdline = i; } - grid_puts(&default_grid, buffer, row, this_ru_col + off, attr); - i = redraw_cmdline; - grid_fill(&default_grid, row, row + 1, - this_ru_col + off + (int)STRLEN(buffer), off + width, fillchar, - fillchar, attr); - // don't redraw the cmdline because of showing the ruler - redraw_cmdline = i; wp->w_ru_cursor = wp->w_cursor; wp->w_ru_virtcol = wp->w_virtcol; wp->w_ru_empty = empty_line; diff --git a/src/nvim/ui.c b/src/nvim/ui.c index 9b2f9c6fe6..16370f2d10 100644 --- a/src/nvim/ui.c +++ b/src/nvim/ui.c @@ -200,6 +200,10 @@ void ui_refresh(void) screen_resize(width, height); p_lz = save_p_lz; + if (ext_widgets[kUIMessages]) { + p_ch = 0; + command_height(); + } ui_mode_info_set(); pending_mode_update = true; ui_cursor_shape(); @@ -380,6 +384,8 @@ void ui_flush(void) { cmdline_ui_flush(); win_ui_flush(); + msg_ext_ui_flush(); + if (pending_cursor_update) { ui_call_grid_cursor_goto(cursor_grid_handle, cursor_row, cursor_col); pending_cursor_update = false; diff --git a/src/nvim/ui.h b/src/nvim/ui.h index 49a30fe78b..490cc930b1 100644 --- a/src/nvim/ui.h +++ b/src/nvim/ui.h @@ -14,7 +14,8 @@ typedef enum { kUIPopupmenu, kUITabline, kUIWildmenu, -#define kUIGlobalCount (kUIWildmenu+1) + kUIMessages, +#define kUIGlobalCount kUILinegrid kUILinegrid, kUIMultigrid, kUIHlState, @@ -27,6 +28,7 @@ EXTERN const char *ui_ext_names[] INIT(= { "ext_popupmenu", "ext_tabline", "ext_wildmenu", + "ext_messages", "ext_linegrid", "ext_multigrid", "ext_hlstate", diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 5c0fb8f88d..7d99f9725c 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -1277,8 +1277,9 @@ describe('API', function() ext_wildmenu = false, ext_linegrid = screen._options.ext_linegrid or false, ext_multigrid = false, - ext_hlstate=false, - ext_termcolors=false, + ext_hlstate = false, + ext_termcolors = false, + ext_messages = false, height = 4, rgb = true, width = 20, diff --git a/test/functional/ui/messages_spec.lua b/test/functional/ui/messages_spec.lua new file mode 100644 index 0000000000..388c6b3e95 --- /dev/null +++ b/test/functional/ui/messages_spec.lua @@ -0,0 +1,632 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') +local clear, feed = helpers.clear, helpers.feed +local eval = helpers.eval +local eq = helpers.eq +local command = helpers.command + + +describe('ui/ext_messages', function() + local screen + + before_each(function() + clear() + screen = Screen.new(25, 5) + screen:attach({rgb=true, ext_messages=true, ext_popupmenu=true}) + screen:set_default_attr_ids({ + [1] = {bold = true, foreground = Screen.colors.Blue1}, + [2] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red}, + [3] = {bold = true}, + [4] = {bold = true, foreground = Screen.colors.SeaGreen4}, + [5] = {foreground = Screen.colors.Blue1}, + [6] = {bold = true, reverse = true}, + }) + end) + + it('supports :echoerr', function() + feed(':echoerr "raa"<cr>') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], messages={{ + content = {{"raa", 2}}, + kind = "echoerr", + }}} + + -- cmdline in a later input cycle clears error message + feed(':') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], cmdline={{ + firstc = ":", + content = {{ "" }}, + pos = 0, + }}} + + + feed('echoerr "bork" | echoerr "fail"<cr>') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], messages={{ + content = {{ "bork", 2 }}, + kind = "echoerr" + }, { + content = {{ "fail", 2 }}, + kind = "echoerr" + }, { + content = {{ "Press ENTER or type command to continue", 4 }}, + kind = "return_prompt" + }}} + + feed(':echoerr "extrafail"<cr>') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], messages={{ + content = { { "bork", 2 } }, + kind = "echoerr" + }, { + content = { { "fail", 2 } }, + kind = "echoerr" + }, { + content = { { "extrafail", 2 } }, + kind = "echoerr" + }, { + content = { { "Press ENTER or type command to continue", 4 } }, + kind = "return_prompt" + }}} + + feed('<cr>') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]]} + + -- cmdline without interleaving wait/display keeps the error message + feed(':echoerr "problem" | let x = input("foo> ")<cr>') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], messages={{ + content = {{ "problem", 2 }}, + kind = "echoerr" + }}, cmdline={{ + prompt = "foo> ", + content = {{ "" }}, + pos = 0, + }}} + + feed('solution<cr>') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]]} + eq('solution', eval('x')) + + feed(":messages<cr>") + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], messages={ + {kind="echoerr", content={{"raa", 2}}}, + {kind="echoerr", content={{"bork", 2}}}, + {kind="echoerr", content={{"fail", 2}}}, + {kind="echoerr", content={{"extrafail", 2}}}, + {kind="echoerr", content={{"problem", 2}}} + }} + end) + + it('supports showmode', function() + command('imap <f2> <cmd>echomsg "stuff"<cr>') + feed('i') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], showmode={{"-- INSERT --", 3}}} + + feed('alphpabet<cr>alphanum<cr>') + screen:expect{grid=[[ + alphpabet | + alphanum | + ^ | + {1:~ }| + {1:~ }| + ]], showmode={ { "-- INSERT --", 3 } }} + + feed('<c-x>') + screen:expect{grid=[[ + alphpabet | + alphanum | + ^ | + {1:~ }| + {1:~ }| + ]], showmode={ { "-- ^X mode (^]^D^E^F^I^K^L^N^O^Ps^U^V^Y)", 3 } }} + + feed('<c-p>') + screen:expect{grid=[[ + alphpabet | + alphanum | + alphanum^ | + {1:~ }| + {1:~ }| + ]], popupmenu={ + anchor = { 2, 0 }, + items = { { "alphpabet", "", "", "" }, { "alphanum", "", "", "" } }, + pos = 1 + }, showmode={ { "-- Keyword Local completion (^N^P) ", 3 }, { "match 1 of 2", 4 } }} + + -- echomsg and showmode don't overwrite each other, this is the same + -- as the TUI behavior with cmdheight=2 or larger. + feed('<f2>') + screen:expect{grid=[[ + alphpabet | + alphanum | + alphanum^ | + {1:~ }| + {1:~ }| + ]], popupmenu={ + anchor = { 2, 0 }, + items = { { "alphpabet", "", "", "" }, { "alphanum", "", "", "" } }, + pos = 1 + }, messages={ { + content = { { "stuff" } }, + kind = "echomsg" + } }, showmode={ { "-- Keyword Local completion (^N^P) ", 3 }, { "match 1 of 2", 4 } }} + + feed('<c-p>') + screen:expect{grid=[[ + alphpabet | + alphanum | + alphpabet^ | + {1:~ }| + {1:~ }| + ]], popupmenu={ + anchor = { 2, 0 }, + items = { { "alphpabet", "", "", "" }, { "alphanum", "", "", "" } }, + pos = 0 + }, messages={ { + content = { { "stuff" } }, + kind = "echomsg" + } }, showmode={ { "-- Keyword Local completion (^N^P) ", 3 }, { "match 2 of 2", 4 } }} + + feed("<esc>:messages<cr>") + screen:expect{grid=[[ + alphpabet | + alphanum | + alphpabe^t | + {1:~ }| + {1:~ }| + ]], messages={ + {kind="echomsg", content={{"stuff"}}}, + }} + end) + + it('supports showmode with recording message', function() + feed('qq') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], showmode={ { "recording @q", 3 } }} + + feed('i') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], showmode={ { "-- INSERT --recording @q", 3 } }} + + feed('<esc>') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], showmode={ { "recording @q", 3 } }} + + feed('q') + screen:expect([[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]]) + end) + + it('shows recording message with noshowmode', function() + command("set noshowmode") + feed('qq') + -- also check mode to avoid immediate success + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], showmode={ { "recording @q", 3 } }, mode="normal"} + + feed('i') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], showmode={ { "recording @q", 3 } }, mode="insert"} + + feed('<esc>') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], showmode={ { "recording @q", 3 } }, mode="normal"} + + feed('q') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], mode="normal"} + end) + + it('supports showcmd and ruler', function() + command('set showcmd ruler') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], ruler={ { "0,0-1 All" } }} + feed('i') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], showmode={ { "-- INSERT --", 3 } }, ruler={ { "0,1 All" } }} + feed('abcde<cr>12345<esc>') + screen:expect{grid=[[ + abcde | + 1234^5 | + {1:~ }| + {1:~ }| + {1:~ }| + ]], ruler={ { "2,5 All" } }} + feed('d') + screen:expect{grid=[[ + abcde | + 1234^5 | + {1:~ }| + {1:~ }| + {1:~ }| + ]], showcmd={ { "d" } }, ruler={ { "2,5 All" } }} + feed('<esc>^') + screen:expect{grid=[[ + abcde | + ^12345 | + {1:~ }| + {1:~ }| + {1:~ }| + ]], ruler={ { "2,1 All" } }} + feed('d') + screen:expect{grid=[[ + abcde | + ^12345 | + {1:~ }| + {1:~ }| + {1:~ }| + ]], showcmd={ { "d" } }, ruler={ { "2,1 All" } }} + feed('i') + screen:expect{grid=[[ + abcde | + ^12345 | + {1:~ }| + {1:~ }| + {1:~ }| + ]], showcmd={ { "di" } }, ruler={ { "2,1 All" } }} + feed('w') + screen:expect{grid=[[ + abcde | + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + ]], ruler={ { "2,0-1 All" } }} + + -- when ruler is part of statusline it is not externalized. + -- this will be added as part of future ext_statusline support + command("set laststatus=2") + screen:expect([[ + abcde | + ^ | + {1:~ }| + {1:~ }| + {6:<o Name] [+] 2,0-1 All}| + ]]) + end) + + it('keeps history of message of different kinds', function() + feed(':echomsg "howdy"<cr>') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], messages={{ + content = {{ "howdy" }}, kind = "echomsg"} + }} + + -- always test a message without kind. If this one gets promoted to a + -- category, add a new message without kind. + feed('<c-c>') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], messages={{ + content = {{ "Type :qa! and press <Enter> to abandon all changes and exit Nvim" }}, + kind = ""} + }} + + feed(':echoerr "bork"<cr>') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], messages={{ + content = {{ "bork", 2 }}, kind = "echoerr"} + }} + + feed(':echo "xyz"<cr>') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], messages={{ + content = {{ "xyz" }}, kind = "echo"} + }} + + feed(':call nosuchfunction()<cr>') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], messages={{ + content = {{ "E117: Unknown function: nosuchfunction", 2 }}, + kind = "emsg"} + }} + + feed(':messages<cr>') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], messages={ + {kind="echomsg", content={{"howdy"}}}, + {kind="", content={{"Type :qa! and press <Enter> to abandon all changes and exit Nvim"}}}, + {kind="echoerr", content={{"bork", 2}}}, + {kind="emsg", content={{"E117: Unknown function: nosuchfunction", 2}}} + }} + end) + + it('implies ext_cmdline and ignores cmdheight', function() + eq(0, eval('&cmdheight')) + feed(':set cmdheight=1') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], cmdline={{ + content = { { "set cmdheight=1" } }, + firstc = ":", + pos = 15 } + }} + + feed('<cr>') + screen:expect([[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]]) + eq(0, eval('&cmdheight')) + + -- normally this would be an error + feed(':set cmdheight=0') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], cmdline={{ + content = { { "set cmdheight=0" } }, + firstc = ":", + pos = 15 } + }} + feed('<cr>') + screen:expect([[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]]) + eq(0, eval('&cmdheight')) + end) + + it('supports multiline messages', function() + feed(':lua error("such\\nmultiline\\nerror")<cr>') + screen:expect{grid=[[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]], messages={{ + content = {{'E5105: Error while calling lua chunk: [string "<VimL compiled string>"]:1: such\nmultiline\nerror', 2}}, + kind = "emsg" + }}} + end) +end) + +describe('ui/ext_messages', function() + local screen + + before_each(function() + clear{headless=false, args={"--cmd", "set shortmess-=I"}} + screen = Screen.new(80, 24) + screen:attach({rgb=true, ext_messages=true, ext_popupmenu=true}) + screen:set_default_attr_ids({ + [1] = {bold = true, foreground = Screen.colors.Blue1}, + [2] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red}, + [3] = {bold = true}, + [4] = {bold = true, foreground = Screen.colors.SeaGreen4}, + [5] = {foreground = Screen.colors.Blue1}, + }) + end) + + it('supports intro screen', function() + -- intro message is not externalized. But check that it still works. + -- Note parts of it depends on version or is indeterministic. We ignore those parts. + screen:expect([[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {IGNORE}| + {1:~ }| + {1:~ }Nvim is open source and freely distributable{1: }| + {1:~ }https://neovim.io/#chat{1: }| + {1:~ }| + {1:~ }type :help nvim{5:<Enter>} if you are new! {1: }| + {1:~ }type :checkhealth{5:<Enter>} to optimize Nvim{1: }| + {1:~ }type :q{5:<Enter>} to exit {1: }| + {1:~ }type :help{5:<Enter>} for help {1: }| + {1:~ }| + {IGNORE}| + {IGNORE}| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]]) + + feed("<c-l>") + screen:expect([[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ]]) + + feed(":intro<cr>") + screen:expect{grid=[[ + | + | + | + | + | + | + {IGNORE}| + | + Nvim is open source and freely distributable | + https://neovim.io/#chat | + | + type :help nvim{5:<Enter>} if you are new! | + type :checkhealth{5:<Enter>} to optimize Nvim | + type :q{5:<Enter>} to exit | + type :help{5:<Enter>} for help | + | + {IGNORE}| + {IGNORE}| + | + | + | + | + | + | + ]], messages={ + {content = { { "Press ENTER or type command to continue", 4 } }, kind = "return_prompt" } + }} + end) +end) diff --git a/test/functional/ui/options_spec.lua b/test/functional/ui/options_spec.lua index 966669fa95..ed630259be 100644 --- a/test/functional/ui/options_spec.lua +++ b/test/functional/ui/options_spec.lua @@ -28,6 +28,7 @@ describe('ui receives option updates', function() ext_linegrid=false, ext_hlstate=false, ext_multigrid=false, + ext_messages=false, ext_termcolors=false, } diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua index 038bf48839..2eae549ebd 100644 --- a/test/functional/ui/screen.lua +++ b/test/functional/ui/screen.lua @@ -159,6 +159,11 @@ function Screen.new(width, height) wildmenu_selected = nil, win_position = {}, _session = nil, + messages = {}, + msg_history = {}, + showmode = {}, + showcmd = {}, + ruler = {}, _default_attr_ids = nil, _default_attr_ignore = nil, _mouse_enabled = true, @@ -250,7 +255,8 @@ end -- canonical order of ext keys, used to generate asserts local ext_keys = { - 'popupmenu', 'cmdline', 'cmdline_block', 'wildmenu_items', 'wildmenu_pos' + 'popupmenu', 'cmdline', 'cmdline_block', 'wildmenu_items', 'wildmenu_pos', + 'messages', 'showmode', 'showcmd', 'ruler', } -- Asserts that the screen state eventually matches an expected state @@ -392,7 +398,7 @@ function Screen:expect(expected, attr_ids, attr_ignore) .. ') differs from configured height(' .. #actual_rows .. ') of Screen.' end for i = 1, #actual_rows do - if expected_rows[i] ~= actual_rows[i] then + if expected_rows[i] ~= actual_rows[i] and expected_rows[i] ~= "{IGNORE}|" then local msg_expected_rows = {} for j = 1, #expected_rows do msg_expected_rows[j] = expected_rows[j] @@ -917,7 +923,7 @@ function Screen:_handle_option_set(name, value) end function Screen:_handle_popupmenu_show(items, selected, row, col) - self.popupmenu = {items=items,pos=selected, anchor={row, col}} + self.popupmenu = {items=items, pos=selected, anchor={row, col}} end function Screen:_handle_popupmenu_select(selected) @@ -973,6 +979,34 @@ function Screen:_handle_wildmenu_hide() self.wildmenu_items, self.wildmenu_pos = nil, nil end +function Screen:_handle_msg_show(kind, chunks, replace_last) + local pos = #self.messages + if not replace_last or pos == 0 then + pos = pos + 1 + end + self.messages[pos] = {kind=kind, content=chunks} +end + +function Screen:_handle_msg_clear() + self.messages = {} +end + +function Screen:_handle_msg_showcmd(msg) + self.showcmd = msg +end + +function Screen:_handle_msg_showmode(msg) + self.showmode = msg +end + +function Screen:_handle_msg_ruler(msg) + self.ruler = msg +end + +function Screen:_handle_msg_history_show(entries) + self.msg_history = entries +end + function Screen:_clear_block(grid, top, bot, left, right) for i = top, bot do self:_clear_row_section(grid, i, left, right) @@ -1057,12 +1091,27 @@ function Screen:_extstate_repr(attr_state) cmdline_block[i] = self:_chunks_repr(entry, attr_state) end + local messages = {} + for i, entry in ipairs(self.messages) do + messages[i] = {kind=entry.kind, content=self:_chunks_repr(entry.content, attr_state)} + end + + local msg_history = {} + for i, entry in ipairs(self.msg_history) do + messages[i] = {kind=entry[1], content=self:_chunks_repr(entry[2], attr_state)} + end + return { popupmenu=self.popupmenu, cmdline=cmdline, cmdline_block=cmdline_block, wildmenu_items=self.wildmenu_items, wildmenu_pos=self.wildmenu_pos, + messages=messages, + showmode=self:_chunks_repr(self.showmode, attr_state), + showcmd=self:_chunks_repr(self.showcmd, attr_state), + ruler=self:_chunks_repr(self.ruler, attr_state), + msg_history=msg_history, } end |