// This is an open source non-commercial project. Dear PVS-Studio, please check // it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com /* * message.c: functions for displaying messages on the command line */ #include #include #include #include #include #include "nvim/vim.h" #include "nvim/ascii.h" #include "nvim/assert.h" #include "nvim/message.h" #include "nvim/charset.h" #include "nvim/eval.h" #include "nvim/ex_eval.h" #include "nvim/ex_docmd.h" #include "nvim/fileio.h" #include "nvim/func_attr.h" #include "nvim/getchar.h" #include "nvim/main.h" #include "nvim/mbyte.h" #include "nvim/memory.h" #include "nvim/misc1.h" #include "nvim/keymap.h" #include "nvim/garray.h" #include "nvim/ops.h" #include "nvim/option.h" #include "nvim/normal.h" #include "nvim/regexp.h" #include "nvim/screen.h" #include "nvim/strings.h" #include "nvim/syntax.h" #include "nvim/highlight.h" #include "nvim/ui.h" #include "nvim/ui_compositor.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 * store the displayed text and remember where screen lines start. */ typedef struct msgchunk_S msgchunk_T; struct msgchunk_S { msgchunk_T *sb_next; msgchunk_T *sb_prev; char sb_eol; /* TRUE when line ends after this text */ int sb_msg_col; /* column in which text starts */ int sb_attr; /* text attributes */ char_u sb_text[1]; /* text to be displayed, actually longer */ }; /* Magic chars used in confirm dialog strings */ #define DLG_BUTTON_SEP '\n' #define DLG_HOTKEY_CHAR '&' static int confirm_msg_used = FALSE; /* displaying confirm_msg */ #ifdef INCLUDE_GENERATED_DECLARATIONS # include "message.c.generated.h" #endif static char_u *confirm_msg = NULL; /* ":confirm" message */ static char_u *confirm_msg_tail; /* tail of confirm_msg */ MessageHistoryEntry *first_msg_hist = NULL; MessageHistoryEntry *last_msg_hist = NULL; static int msg_hist_len = 0; static FILE *verbose_fd = NULL; static int verbose_did_open = FALSE; /* * When writing messages to the screen, there are many different situations. * A number of variables is used to remember the current state: * msg_didany TRUE when messages were written since the last time the * user reacted to a prompt. * Reset: After hitting a key for the hit-return prompt, * hitting for the command line or input(). * Set: When any message is written to the screen. * msg_didout TRUE when something was written to the current line. * Reset: When advancing to the next line, when the current * text can be overwritten. * Set: When any message is written to the screen. * msg_nowait No extra delay for the last drawn message. * Used in normal_cmd() before the mode message is drawn. * emsg_on_display There was an error message recently. Indicates that there * should be a delay before redrawing. * msg_scroll The next message should not overwrite the current one. * msg_scrolled How many lines the screen has been scrolled (because of * messages). Used in update_screen() to scroll the screen * back. Incremented each time the screen scrolls a line. * msg_scrolled_ign TRUE when msg_scrolled is non-zero and msg_puts_attr() * writes something without scrolling should not make * need_wait_return to be set. This is a hack to make ":ts" * work without an extra prompt. * lines_left Number of lines available for messages before the * more-prompt is to be given. -1 when not set. * need_wait_return TRUE when the hit-return prompt is needed. * Reset: After giving the hit-return prompt, when the user * has answered some other prompt. * Set: When the ruler or typeahead display is overwritten, * scrolling the screen for some message. * keep_msg Message to be displayed after redrawing the screen, in * main_loop(). * 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 size_t msg_ext_cur_len = 0; 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; static int msg_grid_pos_at_flush = 0; static int msg_grid_scroll_discount = 0; static void ui_ext_msg_set_pos(int row, bool scrolled) { char buf[MAX_MCO]; size_t size = utf_char2bytes(curwin->w_p_fcs_chars.msgsep, (char_u *)buf); buf[size] = '\0'; ui_call_msg_set_pos(msg_grid.handle, row, scrolled, (String){ .data = buf, .size = size }); } void msg_grid_set_pos(int row, bool scrolled) { if (!msg_grid.throttled) { ui_ext_msg_set_pos(row, scrolled); msg_grid_pos_at_flush = row; } msg_grid_pos = row; if (msg_grid.chars) { msg_grid_adj.row_offset = -row; } } bool msg_use_grid(void) { return default_grid.chars && msg_use_msgsep() && !ui_has(kUIMessages); } void msg_grid_validate(void) { grid_assign_handle(&msg_grid); bool should_alloc = msg_use_grid(); if (should_alloc && (msg_grid.Rows != Rows || msg_grid.Columns != Columns || !msg_grid.chars)) { // TODO(bfredl): eventually should be set to "invalid". I e all callers // will use the grid including clear to EOS if necessary. grid_alloc(&msg_grid, Rows, Columns, false, true); xfree(msg_grid.dirty_col); msg_grid.dirty_col = xcalloc(Rows, sizeof(*msg_grid.dirty_col)); // Tricky: allow resize while pager is active int pos = msg_scrolled ? msg_grid_pos : Rows - p_ch; ui_comp_put_grid(&msg_grid, pos, 0, msg_grid.Rows, msg_grid.Columns, false, true); ui_call_grid_resize(msg_grid.handle, msg_grid.Columns, msg_grid.Rows); msg_grid.throttled = false; // don't throttle in 'cmdheight' area msg_scrolled_at_flush = msg_scrolled; msg_grid.focusable = false; if (!msg_scrolled) { msg_grid_set_pos(Rows - p_ch, false); } } else if (!should_alloc && msg_grid.chars) { ui_comp_remove_grid(&msg_grid); grid_free(&msg_grid); XFREE_CLEAR(msg_grid.dirty_col); ui_call_grid_destroy(msg_grid.handle); msg_grid.throttled = false; msg_grid_adj.row_offset = 0; redraw_cmdline = true; } else if (msg_grid.chars && !msg_scrolled && msg_grid_pos != Rows - p_ch) { msg_grid_set_pos(Rows - p_ch, false); } if (msg_grid.chars && cmdline_row < msg_grid_pos) { // TODO(bfredl): this should already be the case, but fails in some // "batched" executions where compute_cmdrow() use stale positions or // something. cmdline_row = msg_grid_pos; } } /* * msg(s) - displays the string 's' on the status line * When terminal not initialized (yet) mch_errmsg(..) is used. * return TRUE if wait_return not called */ int msg(char_u *s) { return msg_attr_keep(s, 0, false, false); } /// Like msg() but keep it silent when 'verbosefile' is set. int verb_msg(char *s) { verbose_enter(); int n = msg_attr_keep((char_u *)s, 0, false, false); verbose_leave(); return n; } int msg_attr(const char *s, const int attr) FUNC_ATTR_NONNULL_ARG(1) { return msg_attr_keep((char_u *)s, attr, false, false); } /// similar to msg_outtrans_attr, but support newlines and tabs. void msg_multiline_attr(const char *s, int attr, bool check_int) FUNC_ATTR_NONNULL_ALL { const char *next_spec = s; while (next_spec != NULL) { if (check_int && got_int) { return; } next_spec = strpbrk(s, "\t\n\r"); if (next_spec != NULL) { // Printing all char that are before the char found by strpbrk msg_outtrans_len_attr((char_u *)s, next_spec - s, attr); if (*next_spec != TAB) { msg_clr_eos(); } msg_putchar_attr((uint8_t)(*next_spec), attr); s = next_spec + 1; } } // Print the rest of the message. We know there is no special // character because strpbrk returned NULL if (*s != NUL) { msg_outtrans_attr((char_u *)s, attr); } } /// @param keep set keep_msg if it doesn't scroll bool msg_attr_keep(char_u *s, int attr, bool keep, bool multiline) FUNC_ATTR_NONNULL_ALL { static int entered = 0; int retval; char_u *buf = NULL; if (keep && multiline) { // Not implemented. 'multiline' is only used by nvim-added messages, // which should avoid 'keep' behavior (just show the message at // the correct time already). abort(); } // Skip messages not match ":filter pattern". // Don't filter when there is an error. if (!emsg_on_display && message_filtered(s)) { return true; } if (attr == 0) { set_vim_var_string(VV_STATUSMSG, (char *) s, -1); } /* * It is possible that displaying a messages causes a problem (e.g., * when redrawing the window), which causes another message, etc.. To * break this loop, limit the recursiveness to 3 levels. */ if (entered >= 3) return TRUE; ++entered; /* Add message to history (unless it's a repeated kept message or a * truncated message) */ if (s != keep_msg || (*s != '<' && last_msg_hist != NULL && last_msg_hist->msg != NULL && STRCMP(s, last_msg_hist->msg))) { add_msg_hist((const char *)s, -1, attr, multiline); } /* When displaying keep_msg, don't let msg_start() free it, caller must do * that. */ if (s == keep_msg) keep_msg = NULL; /* Truncate the message if needed. */ msg_start(); buf = msg_strtrunc(s, FALSE); if (buf != NULL) s = buf; if (multiline) { msg_multiline_attr((char *)s, attr, false); } else { msg_outtrans_attr(s, attr); } msg_clr_eos(); retval = msg_end(); if (keep && retval && vim_strsize(s) < (int)(Rows - cmdline_row - 1) * Columns + sc_col) { set_keep_msg(s, 0); } xfree(buf); --entered; return retval; } /* * Truncate a string such that it can be printed without causing a scroll. * Returns an allocated string or NULL when no truncating is done. */ char_u * msg_strtrunc ( char_u *s, int force /* always truncate */ ) { char_u *buf = NULL; int len; int room; /* May truncate message to avoid a hit-return prompt */ if ((!msg_scroll && !need_wait_return && shortmess(SHM_TRUNCALL) && !exmode_active && msg_silent == 0 && !ui_has(kUIMessages)) || force) { len = vim_strsize(s); if (msg_scrolled != 0) /* Use all the columns. */ room = (int)(Rows - msg_row) * Columns - 1; else /* Use up to 'showcmd' column. */ room = (int)(Rows - msg_row - 1) * Columns + sc_col - 1; if (len > room && room > 0) { // may have up to 18 bytes per cell (6 per char, up to two // composing chars) len = (room + 2) * 18; buf = xmalloc(len); trunc_string(s, buf, room, len); } } return buf; } /* * Truncate a string "s" to "buf" with cell width "room". * "s" and "buf" may be equal. */ void trunc_string(char_u *s, char_u *buf, int room_in, int buflen) { size_t room = room_in - 3; // "..." takes 3 chars size_t half; size_t len = 0; int e; int i; int n; if (room_in < 3) { room = 0; } half = room / 2; /* First part: Start of the string. */ for (e = 0; len < half && e < buflen; ++e) { if (s[e] == NUL) { /* text fits without truncating! */ buf[e] = NUL; return; } n = ptr2cells(s + e); if (len + n > half) { break; } len += n; buf[e] = s[e]; if (has_mbyte) for (n = (*mb_ptr2len)(s + e); --n > 0; ) { if (++e == buflen) break; buf[e] = s[e]; } } // Last part: End of the string. half = i = (int)STRLEN(s); for (;;) { do { half = half - utf_head_off(s, s + half - 1) - 1; } while (half > 0 && utf_iscomposing(utf_ptr2char(s + half))); n = ptr2cells(s + half); if (len + n > room || half == 0) { break; } len += n; i = half; } if (i <= e + 3) { // text fits without truncating if (s != buf) { len = STRLEN(s); if (len >= (size_t)buflen) { len = buflen - 1; } len = len - e + 1; if (len < 1) { buf[e - 1] = NUL; } else { memmove(buf + e, s + e, len); } } } else if (e + 3 < buflen) { // set the middle and copy the last part memmove(buf + e, "...", (size_t)3); len = STRLEN(s + i) + 1; if (len >= (size_t)buflen - e - 3) { len = buflen - e - 3 - 1; } memmove(buf + e + 3, s + i, len); buf[e + 3 + len - 1] = NUL; } else { // can't fit in the "...", just truncate it buf[e - 1] = NUL; } } /* * Note: Caller of smgs() and smsg_attr() must check the resulting string is * shorter than IOSIZE!!! */ int smsg(char *s, ...) FUNC_ATTR_PRINTF(1, 2) { va_list arglist; va_start(arglist, s); vim_vsnprintf((char *)IObuff, IOSIZE, s, arglist); va_end(arglist); return msg(IObuff); } int smsg_attr(int attr, char *s, ...) FUNC_ATTR_PRINTF(2, 3) { va_list arglist; va_start(arglist, s); vim_vsnprintf((char *)IObuff, IOSIZE, s, arglist); va_end(arglist); return msg_attr((const char *)IObuff, attr); } int smsg_attr_keep(int attr, char *s, ...) FUNC_ATTR_PRINTF(2, 3) { va_list arglist; va_start(arglist, s); vim_vsnprintf((char *)IObuff, IOSIZE, s, arglist); va_end(arglist); return msg_attr_keep(IObuff, attr, true, false); } /* * Remember the last sourcing name/lnum used in an error message, so that it * isn't printed each time when it didn't change. */ static int last_sourcing_lnum = 0; static char_u *last_sourcing_name = NULL; /* * Reset the last used sourcing name/lnum. Makes sure it is displayed again * for the next error message; */ void reset_last_sourcing(void) { XFREE_CLEAR(last_sourcing_name); last_sourcing_lnum = 0; } /* * Return TRUE if "sourcing_name" differs from "last_sourcing_name". */ static int other_sourcing_name(void) { if (sourcing_name != NULL) { if (last_sourcing_name != NULL) return STRCMP(sourcing_name, last_sourcing_name) != 0; return TRUE; } return FALSE; } /// Get the message about the source, as used for an error message /// /// @return [allocated] String with room for one more character. NULL when no /// message is to be given. static char *get_emsg_source(void) FUNC_ATTR_MALLOC FUNC_ATTR_WARN_UNUSED_RESULT { if (sourcing_name != NULL && other_sourcing_name()) { const char *const p = _("Error detected while processing %s:"); const size_t buf_len = STRLEN(sourcing_name) + strlen(p) + 1; char *const buf = xmalloc(buf_len); snprintf(buf, buf_len, p, sourcing_name); return buf; } return NULL; } /// Get the message about the source lnum, as used for an error message. /// /// @return [allocated] String with room for one more character. NULL when no /// message is to be given. static char *get_emsg_lnum(void) FUNC_ATTR_MALLOC FUNC_ATTR_WARN_UNUSED_RESULT { // lnum is 0 when executing a command from the command line // argument, we don't want a line number then if (sourcing_name != NULL && (other_sourcing_name() || sourcing_lnum != last_sourcing_lnum) && sourcing_lnum != 0) { const char *const p = _("line %4ld:"); const size_t buf_len = 20 + strlen(p); char *const buf = xmalloc(buf_len); snprintf(buf, buf_len, p, (long)sourcing_lnum); return buf; } return NULL; } /* * Display name and line number for the source of an error. * Remember the file name and line number, so that for the next error the info * is only displayed if it changed. */ void msg_source(int attr) { no_wait_return++; char *p = get_emsg_source(); if (p != NULL) { msg_attr(p, attr); xfree(p); } p = get_emsg_lnum(); if (p != NULL) { msg_attr(p, HL_ATTR(HLF_N)); xfree(p); last_sourcing_lnum = sourcing_lnum; /* only once for each line */ } /* remember the last sourcing name printed, also when it's empty */ if (sourcing_name == NULL || other_sourcing_name()) { xfree(last_sourcing_name); if (sourcing_name == NULL) last_sourcing_name = NULL; else last_sourcing_name = vim_strsave(sourcing_name); } --no_wait_return; } /* * Return TRUE if not giving error messages right now: * If "emsg_off" is set: no error messages at the moment. * If "msg" is in 'debug': do error message but without side effects. * If "emsg_skip" is set: never do error messages. */ int emsg_not_now(void) { if ((emsg_off > 0 && vim_strchr(p_debug, 'm') == NULL && vim_strchr(p_debug, 't') == NULL) || emsg_skip > 0 ) return TRUE; return FALSE; } static bool emsg_multiline(const char *s, bool multiline) { int attr; int ignore = false; int severe; // Skip this if not giving error messages at the moment. if (emsg_not_now()) { return true; } called_emsg = true; // If "emsg_severe" is TRUE: When an error exception is to be thrown, // prefer this message over previous messages for the same command. severe = emsg_severe; emsg_severe = false; if (!emsg_off || vim_strchr(p_debug, 't') != NULL) { /* * Cause a throw of an error exception if appropriate. Don't display * the error message in this case. (If no matching catch clause will * be found, the message will be displayed later on.) "ignore" is set * when the message should be ignored completely (used for the * interrupt message). */ if (cause_errthrow((char_u *)s, severe, &ignore) == true) { if (!ignore) { did_emsg++; } return true; } // set "v:errmsg", also when using ":silent! cmd" set_vim_var_string(VV_ERRMSG, s, -1); /* * When using ":silent! cmd" ignore error messages. * But do write it to the redirection file. */ if (emsg_silent != 0) { if (!emsg_noredir) { msg_start(); char *p = get_emsg_source(); if (p != NULL) { const size_t p_len = strlen(p); p[p_len] = '\n'; redir_write(p, p_len + 1); xfree(p); } p = get_emsg_lnum(); if (p != NULL) { const size_t p_len = strlen(p); p[p_len] = '\n'; redir_write(p, p_len + 1); xfree(p); } redir_write(s, strlen(s)); } // Log (silent) errors as debug messages. if (sourcing_name != NULL && sourcing_lnum != 0) { DLOG("(:silent) %s (%s (line %ld))", s, sourcing_name, (long)sourcing_lnum); } else { DLOG("(:silent) %s", s); } return true; } // Log editor errors as INFO. if (sourcing_name != NULL && sourcing_lnum != 0) { ILOG("%s (%s (line %ld))", s, sourcing_name, (long)sourcing_lnum); } else { ILOG("%s", s); } ex_exitval = 1; // Reset msg_silent, an error causes messages to be switched back on. msg_silent = 0; cmd_silent = false; if (global_busy) { // break :global command global_busy++; } if (p_eb) { beep_flush(); // also includes flush_buffers() } else { flush_buffers(FLUSH_MINIMAL); // flush internal buffers } did_emsg++; // flag for DoOneCmd() } emsg_on_display = true; // remember there is an error message msg_scroll++; // don't overwrite a previous message attr = HL_ATTR(HLF_E); // set highlight mode for error messages if (msg_scrolled != 0) { need_wait_return = true; // needed in case emsg() is called after } // 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. */ msg_source(attr); // Display the error message itself. msg_nowait = false; // Wait for this msg. return msg_attr_keep((char_u *)s, attr, false, multiline); } /// emsg() - display an error message /// /// Rings the bell, if appropriate, and calls message() to do the real work /// When terminal not initialized (yet) mch_errmsg(..) is used. /// /// @return true if wait_return not called bool emsg(const char_u *s) { return emsg_multiline((const char *)s, false); } void emsg_invreg(int name) { EMSG2(_("E354: Invalid register name: '%s'"), transchar(name)); } /// Print an error message with unknown number of arguments bool emsgf(const char *const fmt, ...) FUNC_ATTR_PRINTF(1, 2) { bool ret; va_list ap; va_start(ap, fmt); ret = emsgfv(fmt, ap); va_end(ap); return ret; } #define MULTILINE_BUFSIZE 8192 bool emsgf_multiline(const char *const fmt, ...) { bool ret; va_list ap; static char errbuf[MULTILINE_BUFSIZE]; if (emsg_not_now()) { return true; } va_start(ap, fmt); vim_vsnprintf(errbuf, sizeof(errbuf), fmt, ap); va_end(ap); ret = emsg_multiline(errbuf, true); return ret; } /// Print an error message with unknown number of arguments static bool emsgfv(const char *fmt, va_list ap) { static char errbuf[IOSIZE]; if (emsg_not_now()) { return true; } vim_vsnprintf(errbuf, sizeof(errbuf), fmt, ap); return emsg((const char_u *)errbuf); } /// Same as emsg(...), but abort on error when ABORT_ON_INTERNAL_ERROR is /// defined. It is used for internal errors only, so that they can be /// detected when fuzzing vim. void iemsg(const char *s) { emsg((char_u *)s); #ifdef ABORT_ON_INTERNAL_ERROR abort(); #endif } /// Same as emsgf(...) but abort on error when ABORT_ON_INTERNAL_ERROR is /// defined. It is used for internal errors only, so that they can be /// detected when fuzzing vim. void iemsgf(const char *s, ...) { va_list ap; va_start(ap, s); (void)emsgfv(s, ap); va_end(ap); #ifdef ABORT_ON_INTERNAL_ERROR abort(); #endif } /// Give an "Internal error" message. void internal_error(char *where) { IEMSG2(_(e_intern2), where); } static void msg_emsgf_event(void **argv) { char *s = argv[0]; (void)emsg((char_u *)s); xfree(s); } void msg_schedule_emsgf(const char *const fmt, ...) FUNC_ATTR_PRINTF(1, 2) { va_list ap; va_start(ap, fmt); vim_vsnprintf((char *)IObuff, IOSIZE, fmt, ap); va_end(ap); char *s = xstrdup((char *)IObuff); multiqueue_put(main_loop.events, msg_emsgf_event, 1, s); } /* * Like msg(), but truncate to a single line if p_shm contains 't', or when * "force" is TRUE. This truncates in another way as for normal messages. * Careful: The string may be changed by msg_may_trunc()! * Returns a pointer to the printed message, if wait_return() not called. */ char_u *msg_trunc_attr(char_u *s, int force, int attr) { int n; // Add message to history before truncating. add_msg_hist((const char *)s, -1, attr, false); s = msg_may_trunc(force, s); msg_hist_off = true; n = msg_attr((const char *)s, attr); msg_hist_off = false; if (n) return s; return NULL; } /* * Check if message "s" should be truncated at the start (for filenames). * Return a pointer to where the truncated message starts. * Note: May change the message by replacing a character with '<'. */ char_u *msg_may_trunc(int force, char_u *s) { int n; int room; room = (int)(Rows - cmdline_row - 1) * Columns + sc_col - 1; if ((force || (shortmess(SHM_TRUNC) && !exmode_active)) && (n = (int)STRLEN(s) - room) > 0) { if (has_mbyte) { int size = vim_strsize(s); /* There may be room anyway when there are multibyte chars. */ if (size <= room) return s; for (n = 0; size >= room; ) { size -= utf_ptr2cells(s + n); n += utfc_ptr2len(s + n); } --n; } s += n; *s = '<'; } return s; } /// @param[in] len Length of s or -1. static void add_msg_hist(const char *s, int len, int attr, bool multiline) { if (msg_hist_off || msg_silent != 0) return; /* Don't let the message history get too big */ while (msg_hist_len > MAX_MSG_HIST_LEN) (void)delete_first_msg(); /* allocate an entry and add the message at the end of the history */ struct msg_hist *p = xmalloc(sizeof(struct msg_hist)); if (len < 0) len = (int)STRLEN(s); /* remove leading and trailing newlines */ while (len > 0 && *s == '\n') { ++s; --len; } while (len > 0 && s[len - 1] == '\n') { len--; } p->msg = (char_u *)xmemdupz(s, (size_t)len); p->next = NULL; p->attr = attr; p->multiline = multiline; p->kind = msg_ext_kind; if (last_msg_hist != NULL) { last_msg_hist->next = p; } last_msg_hist = p; if (first_msg_hist == NULL) { first_msg_hist = last_msg_hist; } msg_hist_len++; } /* * Delete the first (oldest) message from the history. * Returns FAIL if there are no messages. */ int delete_first_msg(void) { struct msg_hist *p; if (msg_hist_len <= 0) return FAIL; p = first_msg_hist; first_msg_hist = p->next; if (first_msg_hist == NULL) { /* history is becoming empty */ assert(msg_hist_len == 1); last_msg_hist = NULL; } xfree(p->msg); xfree(p); --msg_hist_len; return OK; } /// :messages command implementation void ex_messages(void *const eap_p) FUNC_ATTR_NONNULL_ALL { const exarg_T *const eap = (const exarg_T *)eap_p; struct msg_hist *p; int c = 0; if (STRCMP(eap->arg, "clear") == 0) { int keep = eap->addr_count == 0 ? 0 : eap->line2; while (msg_hist_len > keep) { (void)delete_first_msg(); } return; } if (*eap->arg != NUL) { EMSG(_(e_invarg)); return; } p = first_msg_hist; if (eap->addr_count != 0) { // Count total messages for (; p != NULL && !got_int; p = p->next) { c++; } c -= eap->line2; // Skip without number of messages specified for (p = first_msg_hist; p != NULL && !got_int && c > 0; p = p->next, c--) { } } // Display what was not skipped. 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; } } /* * Call this after prompting the user. This will avoid a hit-return message * and a delay. */ void msg_end_prompt(void) { msg_ext_clear_later(); need_wait_return = false; emsg_on_display = false; cmdline_row = msg_row; msg_col = 0; msg_clr_eos(); lines_left = -1; } /// wait for the user to hit a key (normally a return) /// /// if 'redraw' is true, redraw the entire screen NOT_VALID /// if 'redraw' is false, do a normal redraw /// if 'redraw' is -1, don't redraw at all void wait_return(int redraw) { int c; int oldState; int tmpState; int had_got_int; FILE *save_scriptout; if (redraw == true) { redraw_all_later(NOT_VALID); } /* If using ":silent cmd", don't wait for a return. Also don't set * need_wait_return to do it later. */ if (msg_silent != 0) return; /* * When inside vgetc(), we can't wait for a typed character at all. * With the global command (and some others) we only need one return at * the end. Adjust cmdline_row to avoid the next message overwriting the * last one. */ if (vgetc_busy > 0) return; need_wait_return = TRUE; if (no_wait_return) { if (!exmode_active) cmdline_row = msg_row; return; } redir_off = TRUE; /* don't redirect this message */ oldState = State; if (quit_more) { c = CAR; /* just pretend CR was hit */ quit_more = FALSE; got_int = FALSE; } else if (exmode_active) { MSG_PUTS(" "); /* make sure the cursor is on the right line */ c = CAR; /* no need for a return in ex mode */ got_int = FALSE; } else { // Make sure the hit-return prompt is on screen when 'guioptions' was // just changed. screenalloc(); State = HITRETURN; setmouse(); cmdline_row = msg_row; // Avoid the sequence that the user types ":" at the hit-return prompt // to start an Ex command, but the file-changed dialog gets in the // way. if (need_check_timestamps) { check_timestamps(false); } hit_return_msg(); do { /* Remember "got_int", if it is set vgetc() probably returns a * CTRL-C, but we need to loop then. */ had_got_int = got_int; // Don't do mappings here, we put the character back in the // typeahead buffer. no_mapping++; // Temporarily disable Recording. If Recording is active, the // character will be recorded later, since it will be added to the // typebuf after the loop const int save_reg_recording = reg_recording; save_scriptout = scriptout; reg_recording = 0; scriptout = NULL; c = safe_vgetc(); if (had_got_int && !global_busy) { got_int = false; } no_mapping--; reg_recording = save_reg_recording; scriptout = save_scriptout; /* * Allow scrolling back in the messages. * Also accept scroll-down commands when messages fill the screen, * to avoid that typing one 'j' too many makes the messages * disappear. */ if (p_more) { if (c == 'b' || c == 'k' || c == 'u' || c == 'g' || c == K_UP || c == K_PAGEUP) { if (msg_scrolled > Rows) /* scroll back to show older messages */ do_more_prompt(c); else { msg_didout = FALSE; c = K_IGNORE; msg_col = cmdmsg_rl ? Columns - 1 : 0; } if (quit_more) { c = CAR; /* just pretend CR was hit */ quit_more = FALSE; got_int = FALSE; } else if (c != K_IGNORE) { c = K_IGNORE; hit_return_msg(); } } else if (msg_scrolled > Rows - 2 && (c == 'j' || c == 'd' || c == 'f' || c == K_DOWN || c == K_PAGEDOWN)) c = K_IGNORE; } } while ((had_got_int && c == Ctrl_C) || c == K_IGNORE || c == K_LEFTDRAG || c == K_LEFTRELEASE || c == K_MIDDLEDRAG || c == K_MIDDLERELEASE || c == K_RIGHTDRAG || c == K_RIGHTRELEASE || c == K_MOUSELEFT || c == K_MOUSERIGHT || c == K_MOUSEDOWN || c == K_MOUSEUP || (!mouse_has(MOUSE_RETURN) && mouse_row < msg_row && (c == K_LEFTMOUSE || c == K_MIDDLEMOUSE || c == K_RIGHTMOUSE || c == K_X1MOUSE || c == K_X2MOUSE)) ); os_breakcheck(); /* * Avoid that the mouse-up event causes visual mode to start. */ if (c == K_LEFTMOUSE || c == K_MIDDLEMOUSE || c == K_RIGHTMOUSE || c == K_X1MOUSE || c == K_X2MOUSE) (void)jump_to_mouse(MOUSE_SETPOS, NULL, 0); else if (vim_strchr((char_u *)"\r\n ", c) == NULL && c != Ctrl_C) { /* Put the character back in the typeahead buffer. Don't use the * stuff buffer, because lmaps wouldn't work. */ ins_char_typebuf(c); do_redraw = true; // need a redraw even though there is // typeahead } } redir_off = false; // If the user hits ':', '?' or '/' we get a command line from the next // line. if (c == ':' || c == '?' || c == '/') { if (!exmode_active) cmdline_row = msg_row; skip_redraw = true; // skip redraw once do_redraw = false; msg_ext_keep_after_cmdline = true; } // If the window size changed set_shellsize() will redraw the screen. // Otherwise the screen is only redrawn if 'redraw' is set and no ':' // typed. tmpState = State; State = oldState; // restore State before set_shellsize setmouse(); msg_check(); need_wait_return = false; did_wait_return = true; emsg_on_display = false; // can delete error message now lines_left = -1; // reset lines_left at next msg_start() reset_last_sourcing(); if (keep_msg != NULL && vim_strsize(keep_msg) >= (Rows - cmdline_row - 1) * Columns + sc_col) { XFREE_CLEAR(keep_msg); // don't redisplay message, it's too long } if (tmpState == SETWSIZE) { /* got resize event while in vgetc() */ ui_refresh(); } else if (!skip_redraw) { if (redraw == true || (msg_scrolled != 0 && redraw != -1)) { redraw_later(VALID); } if (ui_has(kUIMessages)) { msg_ext_clear(true); } } } /* * Write the hit-return prompt. */ static void hit_return_msg(void) { int save_p_more = p_more; p_more = FALSE; /* don't want see this message when scrolling back */ if (msg_didout) /* start on a new line */ msg_putchar('\n'); 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()) { msg_clr_eos(); } p_more = save_p_more; } /* * Set "keep_msg" to "s". Free the old value and check for NULL pointer. */ void set_keep_msg(char_u *s, int attr) { xfree(keep_msg); if (s != NULL && msg_silent == 0) keep_msg = vim_strsave(s); else keep_msg = NULL; keep_msg_more = FALSE; 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. */ void msg_start(void) { int did_return = false; if (!msg_silent) { XFREE_CLEAR(keep_msg); // don't display old message now } if (need_clr_eos) { // Halfway an ":echo" command and getting an (error) message: clear // any text from the command. need_clr_eos = false; msg_clr_eos(); } if (!msg_scroll && full_screen) { // overwrite last message msg_row = cmdline_row; msg_col = cmdmsg_rl ? Columns - 1 : 0; } else if (msg_didout) { // start message on next line msg_putchar('\n'); did_return = TRUE; if (exmode_active != EXMODE_NORMAL) cmdline_row = msg_row; } if (!msg_didany || lines_left < 0) msg_starthere(); if (msg_silent == 0) { 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); } } /* * Note that the current msg position is where messages start. */ void msg_starthere(void) { lines_left = cmdline_row; msg_didany = FALSE; } void msg_putchar(int c) { msg_putchar_attr(c, 0); } void msg_putchar_attr(int c, int attr) { char buf[MB_MAXBYTES + 1]; if (IS_SPECIAL(c)) { buf[0] = (char)K_SPECIAL; buf[1] = (char)K_SECOND(c); buf[2] = (char)K_THIRD(c); buf[3] = NUL; } else { buf[utf_char2bytes(c, (char_u *)buf)] = NUL; } msg_puts_attr(buf, attr); } void msg_outnum(long n) { char buf[20]; snprintf(buf, sizeof(buf), "%ld", n); msg_puts(buf); } void msg_home_replace(char_u *fname) { msg_home_replace_attr(fname, 0); } void msg_home_replace_hl(char_u *fname) { msg_home_replace_attr(fname, HL_ATTR(HLF_D)); } static void msg_home_replace_attr(char_u *fname, int attr) { char_u *name; name = home_replace_save(NULL, fname); msg_outtrans_attr(name, attr); xfree(name); } /* * Output 'len' characters in 'str' (including NULs) with translation * if 'len' is -1, output upto a NUL character. * Use attributes 'attr'. * Return the number of characters it takes on the screen. */ int msg_outtrans(char_u *str) { return msg_outtrans_attr(str, 0); } int msg_outtrans_attr(char_u *str, int attr) { return msg_outtrans_len_attr(str, (int)STRLEN(str), attr); } int msg_outtrans_len(char_u *str, int len) { return msg_outtrans_len_attr(str, len, 0); } /* * Output one character at "p". Return pointer to the next character. * Handles multi-byte characters. */ char_u *msg_outtrans_one(char_u *p, int attr) { int l; if (has_mbyte && (l = (*mb_ptr2len)(p)) > 1) { msg_outtrans_len_attr(p, l, attr); return p + l; } msg_puts_attr((const char *)transchar_byte(*p), attr); return p + 1; } int msg_outtrans_len_attr(char_u *msgstr, int len, int attr) { int retval = 0; const char *str = (const char *)msgstr; const char *plain_start = (const char *)msgstr; char_u *s; int mb_l; int c; /* if MSG_HIST flag set, add message to history */ if (attr & MSG_HIST) { add_msg_hist(str, len, attr, false); attr &= ~MSG_HIST; } // If the string starts with a composing character first draw a space on // which the composing char can be drawn. if (enc_utf8 && utf_iscomposing(utf_ptr2char(msgstr))) { msg_puts_attr(" ", attr); } /* * Go over the string. Special characters are translated and printed. * Normal characters are printed several at a time. */ while (--len >= 0) { // Don't include composing chars after the end. mb_l = utfc_ptr2len_len((char_u *)str, len + 1); if (mb_l > 1) { c = utf_ptr2char((char_u *)str); if (vim_isprintc(c)) { // Printable multi-byte char: count the cells. retval += utf_ptr2cells((char_u *)str); } else { // Unprintable multi-byte char: print the printable chars so // far and the translation of the unprintable char. if (str > plain_start) { msg_puts_attr_len(plain_start, str - plain_start, attr); } plain_start = str + mb_l; msg_puts_attr((const char *)transchar(c), (attr == 0 ? HL_ATTR(HLF_8) : attr)); retval += char2cells(c); } len -= mb_l - 1; str += mb_l; } else { s = transchar_byte((uint8_t)(*str)); if (s[1] != NUL) { // Unprintable char: print the printable chars so far and the // translation of the unprintable char. if (str > plain_start) { msg_puts_attr_len(plain_start, str - plain_start, attr); } plain_start = str + 1; msg_puts_attr((const char *)s, attr == 0 ? HL_ATTR(HLF_8) : attr); retval += (int)STRLEN(s); } else { retval++; } str++; } } if (str > plain_start) { // Print the printable chars at the end. msg_puts_attr_len(plain_start, str - plain_start, attr); } return retval; } void msg_make(char_u *arg) { int i; static char_u *str = (char_u *)"eeffoc", *rs = (char_u *)"Plon#dqg#vxjduB"; arg = skipwhite(arg); for (i = 5; *arg && i >= 0; --i) if (*arg++ != str[i]) break; if (i < 0) { msg_putchar('\n'); for (i = 0; rs[i]; ++i) msg_putchar(rs[i] - 3); } } /// Output the string 'str' upto a NUL character. /// Return the number of characters it takes on the screen. /// /// If K_SPECIAL is encountered, then it is taken in conjunction with the /// following character and shown as , etc. Any other character /// which is not printable shown in <> form. /// If 'from' is TRUE (lhs of a mapping), a space is shown as . /// If a character is displayed in one of these special ways, is also /// highlighted (its highlight name is '8' in the p_hl variable). /// Otherwise characters are not highlighted. /// This function is used to show mappings, where we want to see how to type /// the character/string -- webb int msg_outtrans_special( const char_u *strstart, int from ///< true for LHS of a mapping ) { if (strstart == NULL) { return 0; // Do nothing. } const char_u *str = strstart; int retval = 0; int attr = HL_ATTR(HLF_8); while (*str != NUL) { const char *string; // Leading and trailing spaces need to be displayed in <> form. if ((str == strstart || str[1] == NUL) && *str == ' ') { string = ""; str++; } else { string = str2special((const char **)&str, from, false); } const int len = vim_strsize((char_u *)string); // Highlight special keys msg_puts_attr(string, (len > 1 && (*mb_ptr2len)((char_u *)string) <= 1 ? attr : 0)); retval += len; } return retval; } /// Convert string, replacing key codes with printables /// /// Used for lhs or rhs of mappings. /// /// @param[in] str String to convert. /// @param[in] replace_spaces Convert spaces into ``, normally used fo /// lhs, but not rhs. /// @param[in] replace_lt Convert `<` into ``. /// /// @return [allocated] Converted string. char *str2special_save(const char *const str, const bool replace_spaces, const bool replace_lt) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_MALLOC FUNC_ATTR_NONNULL_RET { garray_T ga; ga_init(&ga, 1, 40); const char *p = str; while (*p != NUL) { ga_concat(&ga, (const char_u *)str2special(&p, replace_spaces, replace_lt)); } ga_append(&ga, NUL); return (char *)ga.ga_data; } /// Convert character, replacing key with printable representation. /// /// @param[in,out] sp String to convert. Is advanced to the next key code. /// @param[in] replace_spaces Convert spaces into , normally used for /// lhs, but not rhs. /// @param[in] replace_lt Convert `<` into ``. /// /// @return Converted key code, in a static buffer. Buffer is always one and the /// same, so save converted string somewhere before running str2special /// for the second time. const char *str2special(const char **const sp, const bool replace_spaces, const bool replace_lt) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_RET { static char buf[7]; // Try to un-escape a multi-byte character. Return the un-escaped // string if it is a multi-byte character. const char *const p = mb_unescape(sp); if (p != NULL) { return p; } const char *str = *sp; int c = (uint8_t)(*str); int modifiers = 0; bool special = false; if (c == K_SPECIAL && str[1] != NUL && str[2] != NUL) { if ((uint8_t)str[1] == KS_MODIFIER) { modifiers = (uint8_t)str[2]; str += 3; c = (uint8_t)(*str); } if (c == K_SPECIAL && str[1] != NUL && str[2] != NUL) { c = TO_SPECIAL((uint8_t)str[1], (uint8_t)str[2]); str += 2; } if (IS_SPECIAL(c) || modifiers) { // Special key. special = true; } } if (!IS_SPECIAL(c)) { const int len = utf_ptr2len((const char_u *)str); // Check for an illegal byte. if (MB_BYTE2LEN((uint8_t)(*str)) > len) { transchar_nonprint((char_u *)buf, c); *sp = str + 1; return buf; } // Since 'special' is TRUE the multi-byte character 'c' will be // processed by get_special_key_name(). c = utf_ptr2char((const char_u *)str); *sp = str + len; } else { *sp = str + 1; } // Make unprintable characters in <> form, also and . if (special || char2cells(c) > 1 || (replace_spaces && c == ' ') || (replace_lt && c == '<')) { return (const char *)get_special_key_name(c, modifiers); } buf[0] = (char)c; buf[1] = NUL; return buf; } /// Convert string, replacing key codes with printables /// /// @param[in] str String to convert. /// @param[out] buf Buffer to save results to. /// @param[in] len Buffer length. void str2specialbuf(const char *sp, char *buf, size_t len) FUNC_ATTR_NONNULL_ALL { while (*sp) { const char *s = str2special(&sp, false, false); const size_t s_len = strlen(s); if (len <= s_len) { break; } memcpy(buf, s, s_len); buf += s_len; len -= s_len; } *buf = NUL; } /* * print line for :print or :list command */ void msg_prt_line(char_u *s, int list) { int c; int col = 0; int n_extra = 0; int c_extra = 0; int c_final = 0; char_u *p_extra = NULL; // init to make SASC shut up int n; int attr = 0; char_u *trail = NULL; int l; if (curwin->w_p_list) { list = true; } // find start of trailing whitespace if (list && curwin->w_p_lcs_chars.trail) { trail = s + STRLEN(s); while (trail > s && ascii_iswhite(trail[-1])) { trail--; } } // output a space for an empty line, otherwise the line will be overwritten if (*s == NUL && !(list && curwin->w_p_lcs_chars.eol != NUL)) { msg_putchar(' '); } while (!got_int) { if (n_extra > 0) { n_extra--; if (n_extra == 0 && c_final) { c = c_final; } else if (c_extra) { c = c_extra; } else { assert(p_extra != NULL); c = *p_extra++; } } else if ((l = utfc_ptr2len(s)) > 1) { col += utf_ptr2cells(s); char buf[MB_MAXBYTES + 1]; if (curwin->w_p_lcs_chars.nbsp != NUL && list && (utf_ptr2char(s) == 160 || utf_ptr2char(s) == 0x202f)) { utf_char2bytes(curwin->w_p_lcs_chars.nbsp, (char_u *)buf); buf[utfc_ptr2len((char_u *)buf)] = NUL; } else { memmove(buf, s, (size_t)l); buf[l] = NUL; } msg_puts(buf); s += l; continue; } else { attr = 0; c = *s++; if (c == TAB && (!list || curwin->w_p_lcs_chars.tab1)) { // tab amount depends on current column n_extra = curbuf->b_p_ts - col % curbuf->b_p_ts - 1; if (!list) { c = ' '; c_extra = ' '; c_final = NUL; } else { c = (n_extra == 0 && curwin->w_p_lcs_chars.tab3) ? curwin->w_p_lcs_chars.tab3 : curwin->w_p_lcs_chars.tab1; c_extra = curwin->w_p_lcs_chars.tab2; c_final = curwin->w_p_lcs_chars.tab3; attr = HL_ATTR(HLF_8); } } else if (c == 160 && list && curwin->w_p_lcs_chars.nbsp != NUL) { c = curwin->w_p_lcs_chars.nbsp; attr = HL_ATTR(HLF_8); } else if (c == NUL && list && curwin->w_p_lcs_chars.eol != NUL) { p_extra = (char_u *)""; c_extra = NUL; c_final = NUL; n_extra = 1; c = curwin->w_p_lcs_chars.eol; attr = HL_ATTR(HLF_AT); s--; } else if (c != NUL && (n = byte2cells(c)) > 1) { n_extra = n - 1; p_extra = transchar_byte(c); c_extra = NUL; c_final = NUL; c = *p_extra++; /* Use special coloring to be able to distinguish from * the same in plain text. */ attr = HL_ATTR(HLF_8); } else if (c == ' ' && trail != NULL && s > trail) { c = curwin->w_p_lcs_chars.trail; attr = HL_ATTR(HLF_8); } else if (c == ' ' && list && curwin->w_p_lcs_chars.space != NUL) { c = curwin->w_p_lcs_chars.space; attr = HL_ATTR(HLF_8); } } if (c == NUL) break; msg_putchar_attr(c, attr); col++; } msg_clr_eos(); } // Use grid_puts() to output one multi-byte character. // Return the pointer "s" advanced to the next character. static char_u *screen_puts_mbyte(char_u *s, int l, int attr) { int cw; attr = hl_combine_attr(HL_ATTR(HLF_MSG), attr); msg_didout = true; // remember that line is not empty cw = utf_ptr2cells(s); if (cw > 1 && (cmdmsg_rl ? msg_col <= 1 : msg_col == Columns - 1)) { // Doesn't fit, print a highlighted '>' to fill it up. msg_screen_putchar('>', HL_ATTR(HLF_AT)); return s; } grid_puts_len(&msg_grid_adj, s, l, msg_row, msg_col, attr); if (cmdmsg_rl) { msg_col -= cw; if (msg_col == 0) { msg_col = Columns; ++msg_row; } } else { msg_col += cw; if (msg_col >= Columns) { msg_col = 0; ++msg_row; } } return s + l; } /* * Output a string to the screen at position msg_row, msg_col. * Update msg_row and msg_col for the next message. */ void msg_puts(const char *s) { msg_puts_attr(s, 0); } void msg_puts_title(const char *s) { msg_puts_attr(s, HL_ATTR(HLF_T)); } /* * Show a message in such a way that it always fits in the line. Cut out a * part in the middle and replace it with "..." when necessary. * Does not handle multi-byte characters! */ void msg_puts_long_attr(char_u *longstr, int attr) { msg_puts_long_len_attr(longstr, (int)STRLEN(longstr), attr); } void msg_puts_long_len_attr(char_u *longstr, int len, int attr) { int slen = len; int room; room = Columns - msg_col; if (len > room && room >= 20) { slen = (room - 3) / 2; msg_outtrans_len_attr(longstr, slen, attr); msg_puts_attr("...", HL_ATTR(HLF_8)); } msg_outtrans_len_attr(longstr + len - slen, slen, attr); } /* * Basic function for writing a message with highlight attributes. */ void msg_puts_attr(const char *const s, const int attr) { msg_puts_attr_len(s, -1, attr); } /// Write a message with highlight attributes /// /// @param[in] str NUL-terminated message string. /// @param[in] len Length of the string or -1. /// @param[in] attr Highlight attribute. void msg_puts_attr_len(const char *const str, const ptrdiff_t len, int attr) FUNC_ATTR_NONNULL_ALL { assert(len < 0 || memchr(str, 0, len) == NULL); // If redirection is on, also write to the redirection file. redir_write(str, len); // Don't print anything when using ":silent cmd". if (msg_silent != 0) { return; } // if MSG_HIST flag set, add message to history if (attr & MSG_HIST) { add_msg_hist(str, (int)len, attr, false); attr &= ~MSG_HIST; } // When writing something to the screen after it has scrolled, requires a // wait-return prompt later. Needed when scrolling, resetting // need_wait_return after some prompt, and then outputting something // without scrolling 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 // If there is no valid screen, use fprintf so we can see error messages. // If termcap is not active, we may be writing in an alternate console // window, cursor positioning may not work correctly (window size may be // different, e.g. for Win32 console) or we just don't know where the // cursor is. if (msg_use_printf()) { int saved_msg_col = msg_col; msg_puts_printf(str, len); if (headless_mode) { msg_col = saved_msg_col; } } if (!msg_use_printf() || (headless_mode && default_grid.chars)) { msg_puts_display((const char_u *)str, len, attr, false); } } /// 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) FUNC_ATTR_PRINTF(2, 3) { static char msgbuf[IOSIZE]; va_list ap; va_start(ap, fmt); const size_t len = vim_vsnprintf(msgbuf, sizeof(msgbuf), fmt, ap); va_end(ap); msg_scroll = true; 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. */ static void msg_puts_display(const char_u *str, int maxlen, int attr, int recurse) { const char_u *s = str; const char_u *t_s = str; // String from "t_s" to "s" is still todo. int t_col = 0; // Screen cells todo, 0 when "t_s" not used. int l; int cw; const char_u *sb_str = str; int sb_col = msg_col; int wrap; 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 size_t len = strnlen((char *)str, maxlen); // -V781 ga_concat_len(&msg_ext_last_chunk, (char *)str, len); msg_ext_cur_len += len; return; } msg_grid_validate(); cmdline_was_last_drawn = redrawing_cmdline; while ((maxlen < 0 || (int)(s - str) < maxlen) && *s != NUL) { // We are at the end of the screen line when: // - When outputting a newline. // - When outputting a character in the last column. if (!recurse && msg_row >= Rows - 1 && (*s == '\n' || (cmdmsg_rl ? (msg_col <= 1 || (*s == TAB && msg_col <= 7) || (utf_ptr2cells(s) > 1 && msg_col <= 2)) : (msg_col + t_col >= Columns - 1 || (*s == TAB && msg_col + t_col >= ((Columns - 1) & ~7)) || (utf_ptr2cells(s) > 1 && msg_col + t_col >= Columns - 2))))) { // The screen is scrolled up when at the last row (some terminals // scroll automatically, some don't. To avoid problems we scroll // ourselves). if (t_col > 0) { // output postponed text t_puts(&t_col, t_s, s, attr); } /* When no more prompt and no more room, truncate here */ if (msg_no_more && lines_left == 0) break; // Scroll the screen up one line. bool has_last_char = (*s >= ' ' && !cmdmsg_rl); msg_scroll_up(!has_last_char); msg_row = Rows - 2; if (msg_col >= Columns) /* can happen after screen resize */ msg_col = Columns - 1; // Display char in last column before showing more-prompt. if (has_last_char) { if (maxlen >= 0) { // Avoid including composing chars after the end. l = utfc_ptr2len_len(s, (int)((str + maxlen) - s)); } else { l = utfc_ptr2len(s); } s = screen_puts_mbyte((char_u *)s, l, attr); did_last_char = true; } else { did_last_char = false; } // Tricky: if last cell will be written, delay the throttle until // after the first scroll. Otherwise we would need to keep track of it. if (has_last_char && msg_do_throttle()) { if (!msg_grid.throttled) { msg_grid_scroll_discount++; } msg_grid.throttled = true; } if (p_more) { // Store text for scrolling back. store_sb_text((char_u **)&sb_str, (char_u *)s, attr, &sb_col, true); } inc_msg_scrolled(); need_wait_return = true; // may need wait_return in main() redraw_cmdline = true; if (cmdline_row > 0 && !exmode_active) { cmdline_row--; } /* * If screen is completely filled and 'more' is set then wait * for a character. */ if (lines_left > 0) --lines_left; if (p_more && lines_left == 0 && State != HITRETURN && !msg_no_more && !exmode_active) { if (do_more_prompt(NUL)) s = confirm_msg_tail; if (quit_more) return; } /* When we displayed a char in last column need to check if there * is still more. */ if (did_last_char) continue; } wrap = *s == '\n' || msg_col + t_col >= Columns || (utf_ptr2cells(s) > 1 && msg_col + t_col >= Columns - 1) ; if (t_col > 0 && (wrap || *s == '\r' || *s == '\b' || *s == '\t' || *s == BELL)) { // Output any postponed text. t_puts(&t_col, t_s, s, attr); } if (wrap && p_more && !recurse) { // Store text for scrolling back. store_sb_text((char_u **)&sb_str, (char_u *)s, attr, &sb_col, true); } if (*s == '\n') { /* go to next line */ msg_didout = FALSE; /* remember that line is empty */ if (cmdmsg_rl) msg_col = Columns - 1; else msg_col = 0; if (++msg_row >= Rows) /* safety check */ msg_row = Rows - 1; } else if (*s == '\r') { /* go to column 0 */ msg_col = 0; } else if (*s == '\b') { /* go to previous char */ if (msg_col) --msg_col; } else if (*s == TAB) { /* translate Tab into spaces */ do { msg_screen_putchar(' ', attr); } while (msg_col & 7); } else if (*s == BELL) { // beep (from ":sh") vim_beep(BO_SH); } else if (*s >= 0x20) { // printable char cw = utf_ptr2cells(s); if (maxlen >= 0) { // avoid including composing chars after the end l = utfc_ptr2len_len(s, (int)((str + maxlen) - s)); } else { l = utfc_ptr2len(s); } // When drawing from right to left or when a double-wide character // doesn't fit, draw a single character here. Otherwise collect // characters and draw them all at once later. if (cmdmsg_rl || (cw > 1 && msg_col + t_col >= Columns - 1)) { if (l > 1) { s = screen_puts_mbyte((char_u *)s, l, attr) - 1; } else { msg_screen_putchar(*s, attr); } } else { /* postpone this character until later */ if (t_col == 0) t_s = s; t_col += cw; s += l - 1; } } ++s; } // Output any postponed text. if (t_col > 0) { t_puts(&t_col, t_s, s, attr); } if (p_more && !recurse) { store_sb_text((char_u **)&sb_str, (char_u *)s, attr, &sb_col, false); } msg_check(); } /// Return true when ":filter pattern" was used and "msg" does not match /// "pattern". bool message_filtered(char_u *msg) { if (cmdmod.filter_regmatch.regprog == NULL) { return false; } bool match = vim_regexec(&cmdmod.filter_regmatch, msg, (colnr_T)0); return cmdmod.filter_force ? match : !match; } /// including horizontal separator int msg_scrollsize(void) { return msg_scrolled + p_ch + 1; } bool msg_use_msgsep(void) { // the full-screen scroll behavior doesn't really make sense with // 'ext_multigrid' return ((dy_flags & DY_MSGSEP) || ui_has(kUIMultigrid)); } bool msg_do_throttle(void) { return msg_use_grid() && !(rdb_flags & RDB_NOTHROTTLE); } /// Scroll the screen up one line for displaying the next message line. void msg_scroll_up(bool may_throttle) { if (may_throttle && msg_do_throttle()) { msg_grid.throttled = true; } msg_did_scroll = true; if (msg_use_msgsep()) { if (msg_grid_pos > 0) { msg_grid_set_pos(msg_grid_pos-1, true); } else { grid_del_lines(&msg_grid, 0, 1, msg_grid.Rows, 0, msg_grid.Columns); memmove(msg_grid.dirty_col, msg_grid.dirty_col+1, (msg_grid.Rows-1) * sizeof(*msg_grid.dirty_col)); msg_grid.dirty_col[msg_grid.Rows-1] = 0; } } else { grid_del_lines(&msg_grid_adj, 0, 1, Rows, 0, Columns); } grid_fill(&msg_grid_adj, Rows-1, Rows, 0, Columns, ' ', ' ', HL_ATTR(HLF_MSG)); } /// Send throttled message output to UI clients /// /// The way message.c uses the grid_xx family of functions is quite inefficient /// relative to the "gridline" UI protocol used by TUI and modern clients. /// For instance scrolling is done one line at a time. By throttling drawing /// on the message grid, we can coalesce scrolling to a single grid_scroll /// per screen update. /// /// NB: The bookkeeping is quite messy, and rests on a bunch of poorly /// documented assumtions. For instance that the message area always grows while /// being throttled, messages are only being output on the last line etc. /// /// Probably message scrollback storage should reimplented as a file_buffer, and /// message scrolling in TUI be reimplemented as a modal floating window. Then /// we get throttling "for free" using standard redraw_win_later code paths. void msg_scroll_flush(void) { if (msg_grid.throttled) { msg_grid.throttled = false; int pos_delta = msg_grid_pos_at_flush - msg_grid_pos; assert(pos_delta >= 0); int delta = MIN(msg_scrolled - msg_scrolled_at_flush, msg_grid.Rows); if (pos_delta > 0) { ui_ext_msg_set_pos(msg_grid_pos, true); } int to_scroll = delta-pos_delta-msg_grid_scroll_discount; assert(to_scroll >= 0); // TODO(bfredl): msg_grid_pos should be 0 already when starting scrolling // but this sometimes fails in "headless" message printing. if (to_scroll > 0 && msg_grid_pos == 0) { ui_call_grid_scroll(msg_grid.handle, 0, Rows, 0, Columns, to_scroll, 0); } for (int i = MAX(Rows-MAX(delta, 1), 0); i < Rows; i++) { int row = i-msg_grid_pos; assert(row >= 0); ui_line(&msg_grid, row, 0, msg_grid.dirty_col[row], msg_grid.Columns, HL_ATTR(HLF_MSG), false); msg_grid.dirty_col[row] = 0; } } msg_scrolled_at_flush = msg_scrolled; msg_grid_scroll_discount = 0; msg_grid_pos_at_flush = msg_grid_pos; } void msg_reset_scroll(void) { if (ui_has(kUIMessages)) { msg_ext_clear(true); return; } // TODO(bfredl): some duplicate logic with update_screen(). Later on // we should properly disentangle message clear with full screen redraw. if (msg_use_grid()) { msg_grid.throttled = false; // TODO(bfredl): risk for extra flicker i e with // "nvim -o has_swap also_has_swap" msg_grid_set_pos(Rows - p_ch, false); clear_cmdline = true; if (msg_grid.chars) { // non-displayed part of msg_grid is considered invalid. for (int i = 0; i < MIN(msg_scrollsize(), msg_grid.Rows); i++) { grid_clear_line(&msg_grid, msg_grid.line_offset[i], (int)msg_grid.Columns, false); } } } else { redraw_all_later(NOT_VALID); } msg_scrolled = 0; msg_scrolled_at_flush = 0; } /* * Increment "msg_scrolled". */ static void inc_msg_scrolled(void) { if (*get_vim_var_str(VV_SCROLLSTART) == NUL) { char *p = (char *) sourcing_name; char *tofree = NULL; // v:scrollstart is empty, set it to the script/function name and line // number if (p == NULL) { p = _("Unknown"); } else { size_t len = strlen(p) + 40; tofree = xmalloc(len); vim_snprintf(tofree, len, _("%s line %" PRId64), p, (int64_t) sourcing_lnum); p = tofree; } set_vim_var_string(VV_SCROLLSTART, p, -1); xfree(tofree); } msg_scrolled++; if (must_redraw < VALID) { must_redraw = VALID; } } static msgchunk_T *last_msgchunk = NULL; // last displayed text typedef enum { SB_CLEAR_NONE = 0, SB_CLEAR_ALL, SB_CLEAR_CMDLINE_BUSY, SB_CLEAR_CMDLINE_DONE } sb_clear_T; // When to clear text on next msg. static sb_clear_T do_clear_sb_text = SB_CLEAR_NONE; /// Store part of a printed message for displaying when scrolling back. static void store_sb_text( char_u **sb_str, // start of string char_u *s, // just after string int attr, int *sb_col, int finish // line ends ) { msgchunk_T *mp; if (do_clear_sb_text == SB_CLEAR_ALL || do_clear_sb_text == SB_CLEAR_CMDLINE_DONE) { clear_sb_text(do_clear_sb_text == SB_CLEAR_ALL); do_clear_sb_text = SB_CLEAR_NONE; } if (s > *sb_str) { mp = xmalloc((sizeof(msgchunk_T) + (s - *sb_str))); mp->sb_eol = finish; mp->sb_msg_col = *sb_col; mp->sb_attr = attr; memcpy(mp->sb_text, *sb_str, s - *sb_str); mp->sb_text[s - *sb_str] = NUL; if (last_msgchunk == NULL) { last_msgchunk = mp; mp->sb_prev = NULL; } else { mp->sb_prev = last_msgchunk; last_msgchunk->sb_next = mp; last_msgchunk = mp; } mp->sb_next = NULL; } else if (finish && last_msgchunk != NULL) last_msgchunk->sb_eol = TRUE; *sb_str = s; *sb_col = 0; } /* * Finished showing messages, clear the scroll-back text on the next message. */ void may_clear_sb_text(void) { do_clear_sb_text = SB_CLEAR_ALL; } /// Starting to edit the command line, do not clear messages now. void sb_text_start_cmdline(void) { do_clear_sb_text = SB_CLEAR_CMDLINE_BUSY; msg_sb_eol(); } /// Ending to edit the command line. Clear old lines but the last one later. void sb_text_end_cmdline(void) { do_clear_sb_text = SB_CLEAR_CMDLINE_DONE; } /// Clear any text remembered for scrolling back. /// When "all" is FALSE keep the last line. /// Called when redrawing the screen. void clear_sb_text(int all) { msgchunk_T *mp; msgchunk_T **lastp; if (all) { lastp = &last_msgchunk; } else { if (last_msgchunk == NULL) { return; } lastp = &last_msgchunk->sb_prev; } while (*lastp != NULL) { mp = (*lastp)->sb_prev; xfree(*lastp); *lastp = mp; } } /* * "g<" command. */ void show_sb_text(void) { msgchunk_T *mp; /* Only show something if there is more than one line, otherwise it looks * weird, typing a command without output results in one line. */ mp = msg_sb_start(last_msgchunk); if (mp == NULL || mp->sb_prev == NULL) { vim_beep(BO_MESS); } else { do_more_prompt('G'); wait_return(FALSE); } } /* * Move to the start of screen line in already displayed text. */ static msgchunk_T *msg_sb_start(msgchunk_T *mps) { msgchunk_T *mp = mps; while (mp != NULL && mp->sb_prev != NULL && !mp->sb_prev->sb_eol) mp = mp->sb_prev; return mp; } /* * Mark the last message chunk as finishing the line. */ void msg_sb_eol(void) { if (last_msgchunk != NULL) last_msgchunk->sb_eol = TRUE; } /* * Display a screen line from previously displayed text at row "row". * Returns a pointer to the text for the next line (can be NULL). */ static msgchunk_T *disp_sb_line(int row, msgchunk_T *smp) { msgchunk_T *mp = smp; char_u *p; for (;; ) { msg_row = row; msg_col = mp->sb_msg_col; p = mp->sb_text; if (*p == '\n') /* don't display the line break */ ++p; msg_puts_display(p, -1, mp->sb_attr, TRUE); if (mp->sb_eol || mp->sb_next == NULL) break; mp = mp->sb_next; } return mp->sb_next; } /* * Output any postponed text for msg_puts_attr_len(). */ static void t_puts(int *t_col, const char_u *t_s, const char_u *s, int attr) { attr = hl_combine_attr(HL_ATTR(HLF_MSG), attr); // Output postponed text. msg_didout = true; // Remember that line is not empty. grid_puts_len(&msg_grid_adj, (char_u *)t_s, (int)(s - t_s), msg_row, msg_col, attr); msg_col += *t_col; *t_col = 0; /* If the string starts with a composing character don't increment the * column position for it. */ if (enc_utf8 && utf_iscomposing(utf_ptr2char(t_s))) --msg_col; if (msg_col >= Columns) { msg_col = 0; ++msg_row; } } // Returns TRUE when messages should be printed to stdout/stderr: // - "batch mode" ("silent mode", -es/-Es) // - no UI and not embedded int msg_use_printf(void) { return !embedded_mode && !ui_active(); } /// Print a message when there is no valid screen. static void msg_puts_printf(const char *str, const ptrdiff_t maxlen) { const char *s = str; char buf[7]; char *p; while ((maxlen < 0 || s - str < maxlen) && *s != NUL) { int len = utf_ptr2len((const char_u *)s); if (!(silent_mode && p_verbose == 0)) { // NL --> CR NL translation (for Unix, not for "--version") p = &buf[0]; if (*s == '\n' && !info_message) { *p++ = '\r'; } memcpy(p, s, len); *(p + len) = '\0'; if (info_message) { mch_msg(buf); } else { mch_errmsg(buf); } } int cw = utf_char2cells(utf_ptr2char((const char_u *)s)); // primitive way to compute the current column if (cmdmsg_rl) { if (*s == '\r' || *s == '\n') { msg_col = Columns - 1; } else { msg_col -= cw; } } else { if (*s == '\r' || *s == '\n') { msg_col = 0; } else { msg_col += cw; } } s += len; } msg_didout = true; // assume that line is not empty } /* * Show the more-prompt and handle the user response. * This takes care of scrolling back and displaying previously displayed text. * When at hit-enter prompt "typed_char" is the already typed character, * otherwise it's NUL. * Returns TRUE when jumping ahead to "confirm_msg_tail". */ static int do_more_prompt(int typed_char) { static bool entered = false; int used_typed_char = typed_char; int oldState = State; int c; int retval = FALSE; int toscroll; bool to_redraw = false; msgchunk_T *mp_last = NULL; msgchunk_T *mp; int i; // We get called recursively when a timer callback outputs a message. In // that case don't show another prompt. Also when at the hit-Enter prompt // and nothing was typed. if (entered || (State == HITRETURN && typed_char == 0)) { return false; } entered = true; if (typed_char == 'G') { /* "g<": Find first line on the last page. */ mp_last = msg_sb_start(last_msgchunk); for (i = 0; i < Rows - 2 && mp_last != NULL && mp_last->sb_prev != NULL; ++i) mp_last = msg_sb_start(mp_last->sb_prev); } State = ASKMORE; setmouse(); if (typed_char == NUL) msg_moremsg(FALSE); for (;; ) { /* * Get a typed character directly from the user. */ if (used_typed_char != NUL) { c = used_typed_char; /* was typed at hit-enter prompt */ used_typed_char = NUL; } else { c = get_keystroke(resize_events); } toscroll = 0; switch (c) { case BS: /* scroll one line back */ case K_BS: case 'k': case K_UP: toscroll = -1; break; case CAR: /* one extra line */ case NL: case 'j': case K_DOWN: toscroll = 1; break; case 'u': /* Up half a page */ toscroll = -(Rows / 2); break; case 'd': /* Down half a page */ toscroll = Rows / 2; break; case 'b': /* one page back */ case K_PAGEUP: toscroll = -(Rows - 1); break; case ' ': /* one extra page */ case 'f': case K_PAGEDOWN: case K_LEFTMOUSE: toscroll = Rows - 1; break; case 'g': /* all the way back to the start */ toscroll = -999999; break; case 'G': /* all the way to the end */ toscroll = 999999; lines_left = 999999; break; case ':': /* start new command line */ if (!confirm_msg_used) { /* Since got_int is set all typeahead will be flushed, but we * want to keep this ':', remember that in a special way. */ typeahead_noflush(':'); cmdline_row = Rows - 1; /* put ':' on this line */ skip_redraw = TRUE; /* skip redraw once */ need_wait_return = FALSE; /* don't wait in main() */ } FALLTHROUGH; case 'q': // quit case Ctrl_C: case ESC: if (confirm_msg_used) { /* Jump to the choices of the dialog. */ retval = TRUE; } else { got_int = TRUE; quit_more = TRUE; } /* When there is some more output (wrapping line) display that * without another prompt. */ lines_left = Rows - 1; break; case K_EVENT: // only resize_events are processed here // Attempt to redraw the screen. sb_text doesn't support reflow // so this only really works for vertical resize. multiqueue_process_events(resize_events); to_redraw = true; break; default: /* no valid response */ msg_moremsg(TRUE); continue; } // code assumes we only do one at a time assert((toscroll == 0) || !to_redraw); if (toscroll != 0 || to_redraw) { if (toscroll < 0 || to_redraw) { // go to start of last line if (mp_last == NULL) { mp = msg_sb_start(last_msgchunk); } else if (mp_last->sb_prev != NULL) { mp = msg_sb_start(mp_last->sb_prev); } else { mp = NULL; } /* go to start of line at top of the screen */ for (i = 0; i < Rows - 2 && mp != NULL && mp->sb_prev != NULL; ++i) mp = msg_sb_start(mp->sb_prev); if (mp != NULL && (mp->sb_prev != NULL || to_redraw)) { // Find line to be displayed at top for (i = 0; i > toscroll; i--) { if (mp == NULL || mp->sb_prev == NULL) { break; } mp = msg_sb_start(mp->sb_prev); if (mp_last == NULL) mp_last = msg_sb_start(last_msgchunk); else mp_last = msg_sb_start(mp_last->sb_prev); } if (toscroll == -1 && !to_redraw) { grid_ins_lines(&msg_grid_adj, 0, 1, Rows, 0, Columns); grid_fill(&msg_grid_adj, 0, 1, 0, Columns, ' ', ' ', HL_ATTR(HLF_MSG)); // display line at top (void)disp_sb_line(0, mp); } else { // redisplay all lines // TODO(bfredl): this case is not optimized (though only concerns // event fragmentization, not unnecessary scroll events). grid_fill(&msg_grid_adj, 0, Rows, 0, Columns, ' ', ' ', HL_ATTR(HLF_MSG)); for (i = 0; mp != NULL && i < Rows - 1; i++) { mp = disp_sb_line(i, mp); ++msg_scrolled; } to_redraw = false; } toscroll = 0; } } else { /* First display any text that we scrolled back. */ while (toscroll > 0 && mp_last != NULL) { if (msg_do_throttle() && !msg_grid.throttled) { // Tricky: we redraw at one line higher than usual. Therefore // the non-flushed area is one line larger. msg_scrolled_at_flush--; msg_grid_scroll_discount++; } // scroll up, display line at bottom msg_scroll_up(true); inc_msg_scrolled(); grid_fill(&msg_grid_adj, Rows-2, Rows-1, 0, Columns, ' ', ' ', HL_ATTR(HLF_MSG)); mp_last = disp_sb_line(Rows - 2, mp_last); toscroll--; } } if (toscroll <= 0) { // displayed the requested text, more prompt again grid_fill(&msg_grid_adj, Rows - 1, Rows, 0, Columns, ' ', ' ', HL_ATTR(HLF_MSG)); msg_moremsg(false); continue; } /* display more text, return to caller */ lines_left = toscroll; } break; } // clear the --more-- message grid_fill(&msg_grid_adj, Rows - 1, Rows, 0, Columns, ' ', ' ', HL_ATTR(HLF_MSG)); redraw_cmdline = true; clear_cmdline = false; mode_displayed = false; State = oldState; setmouse(); if (quit_more) { msg_row = Rows - 1; msg_col = 0; } else if (cmdmsg_rl) { msg_col = Columns - 1; } entered = false; return retval; } #if defined(WIN32) void mch_errmsg(char *str) { assert(str != NULL); wchar_t *utf16str; int r = utf8_to_utf16(str, -1, &utf16str); if (r != 0) { fprintf(stderr, "utf8_to_utf16 failed: %d", r); } else { fwprintf(stderr, L"%ls", utf16str); xfree(utf16str); } } // Give a message. To be used when the UI is not initialized yet. void mch_msg(char *str) { assert(str != NULL); wchar_t *utf16str; int r = utf8_to_utf16(str, -1, &utf16str); if (r != 0) { fprintf(stderr, "utf8_to_utf16 failed: %d", r); } else { wprintf(L"%ls", utf16str); xfree(utf16str); } } #endif // WIN32 /* * Put a character on the screen at the current message position and advance * to the next position. Only for printable ASCII! */ static void msg_screen_putchar(int c, int attr) { attr = hl_combine_attr(HL_ATTR(HLF_MSG), attr); msg_didout = true; // remember that line is not empty grid_putchar(&msg_grid_adj, c, msg_row, msg_col, attr); if (cmdmsg_rl) { if (--msg_col == 0) { msg_col = Columns; ++msg_row; } } else { if (++msg_col >= Columns) { msg_col = 0; ++msg_row; } } } void msg_moremsg(int full) { int attr; char_u *s = (char_u *)_("-- More --"); attr = hl_combine_attr(HL_ATTR(HLF_MSG), HL_ATTR(HLF_M)); grid_puts(&msg_grid_adj, s, Rows - 1, 0, attr); if (full) { grid_puts(&msg_grid_adj, (char_u *) _(" SPACE/d/j: screen/page/line down, b/u/k: up, q: quit "), Rows - 1, vim_strsize(s), attr); } } /* * Repeat the message for the current mode: ASKMORE, EXTERNCMD, CONFIRM or * exmode_active. */ void repeat_message(void) { if (State == ASKMORE) { msg_moremsg(TRUE); /* display --more-- message again */ msg_row = Rows - 1; } else if (State == CONFIRM) { display_confirm_msg(); /* display ":confirm" message again */ msg_row = Rows - 1; } else if (State == EXTERNCMD) { ui_cursor_goto(msg_row, msg_col); /* put cursor back */ } else if (State == HITRETURN || State == SETWSIZE) { if (msg_row == Rows - 1) { /* Avoid drawing the "hit-enter" prompt below the previous one, * overwrite it. Esp. useful when regaining focus and a * FocusGained autocmd exists but didn't draw anything. */ msg_didout = FALSE; msg_col = 0; msg_clr_eos(); } hit_return_msg(); msg_row = Rows - 1; } } /* * Clear from current message position to end of screen. * Skip this when ":silent" was used, no need to clear for redirection. */ void msg_clr_eos(void) { if (msg_silent == 0) msg_clr_eos_force(); } /* * Clear from current message position to end of screen. * Note: msg_col is not updated, so we remember the end of the message * for msg_check(). */ 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 : Columns; if (msg_grid.chars && msg_row < msg_grid_pos) { // TODO(bfredl): ugly, this state should already been validated at this // point. But msg_clr_eos() is called in a lot of places. msg_row = msg_grid_pos; } grid_fill(&msg_grid_adj, msg_row, msg_row + 1, msg_startcol, msg_endcol, ' ', ' ', HL_ATTR(HLF_MSG)); grid_fill(&msg_grid_adj, msg_row + 1, Rows, 0, Columns, ' ', ' ', HL_ATTR(HLF_MSG)); redraw_cmdline = true; // overwritten the command line if (msg_row < Rows-1 || msg_col == (cmdmsg_rl ? Columns : 0)) { clear_cmdline = false; // command line has been cleared mode_displayed = false; // mode cleared or overwritten } } /* * Clear the command line. */ void msg_clr_cmdline(void) { msg_row = cmdline_row; msg_col = 0; msg_clr_eos_force(); } /* * end putting a message on the screen * call wait_return if the message does not fit in the available space * return TRUE if wait_return not called. */ int msg_end(void) { /* * If the string is larger than the window, * or the ruler option is set and we run into it, * we have to redraw the window. * Do not do this if we are abandoning the file or editing the command line. */ if (!exiting && need_wait_return && !(State & CMDLINE)) { wait_return(FALSE); return FALSE; } // NOTE: ui_flush() used to be called here. This had to be removed, as it // inhibited substantial performance improvements. It is assumed that relevant // callers invoke ui_flush() before going into CPU busywork, or restricted // event processing after displaying a message to the user. 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_cur_len = 0; 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. if (ui_has(kUIMessages)) { msg_ext_emit_chunk(); ui_call_msg_showmode(msg_ext_chunks); msg_ext_chunks = (Array)ARRAY_DICT_INIT; msg_ext_cur_len = 0; } } 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_clear_later(void) { if (msg_ext_is_visible()) { msg_ext_need_clear = true; if (must_redraw < VALID) { must_redraw = VALID; } } } void msg_ext_check_clear(void) { // Redraw after cmdline or prompt is expected to clear messages. if (msg_ext_need_clear) { msg_ext_clear(true); msg_ext_need_clear = false; } } bool msg_ext_is_visible(void) { return ui_has(kUIMessages) && msg_ext_visible > 0; } /* * If the written message runs into the shown command or ruler, we have to * wait for hit-return and redraw the window later. */ 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; } } /* * May write a string to the redirection file. * When "maxlen" is -1 write the whole string, otherwise up to "maxlen" bytes. */ static void redir_write(const char *const str, const ptrdiff_t maxlen) { const char_u *s = (char_u *)str; static int cur_col = 0; if (maxlen == 0) { return; } /* Don't do anything for displaying prompts and the like. */ if (redir_off) return; /* If 'verbosefile' is set prepare for writing in that file. */ if (*p_vfile != NUL && verbose_fd == NULL) verbose_open(); if (redirecting()) { /* If the string doesn't start with CR or NL, go to msg_col */ if (*s != '\n' && *s != '\r') { while (cur_col < msg_col) { if (capture_ga) { ga_concat_len(capture_ga, " ", 1); } if (redir_reg) { write_reg_contents(redir_reg, (char_u *)" ", 1, true); } else if (redir_vname) { var_redir_str((char_u *)" ", -1); } else if (redir_fd != NULL) { fputs(" ", redir_fd); } if (verbose_fd != NULL) { fputs(" ", verbose_fd); } cur_col++; } } size_t len = maxlen == -1 ? STRLEN(s) : (size_t)maxlen; if (capture_ga) { ga_concat_len(capture_ga, (const char *)str, len); } if (redir_reg) { write_reg_contents(redir_reg, s, len, true); } if (redir_vname) { var_redir_str((char_u *)s, maxlen); } // Write and adjust the current column. while (*s != NUL && (maxlen < 0 || (int)(s - (const char_u *)str) < maxlen)) { if (!redir_reg && !redir_vname && !capture_ga) { if (redir_fd != NULL) { putc(*s, redir_fd); } } if (verbose_fd != NULL) { putc(*s, verbose_fd); } if (*s == '\r' || *s == '\n') { cur_col = 0; } else if (*s == '\t') { cur_col += (8 - cur_col % 8); } else { cur_col++; } s++; } if (msg_silent != 0) /* should update msg_col */ msg_col = cur_col; } } int redirecting(void) { return redir_fd != NULL || *p_vfile != NUL || redir_reg || redir_vname || capture_ga != NULL; } /* * Before giving verbose message. * Must always be called paired with verbose_leave()! */ void verbose_enter(void) { if (*p_vfile != NUL) ++msg_silent; } /* * After giving verbose message. * Must always be called paired with verbose_enter()! */ void verbose_leave(void) { if (*p_vfile != NUL) if (--msg_silent < 0) msg_silent = 0; } /* * Like verbose_enter() and set msg_scroll when displaying the message. */ void verbose_enter_scroll(void) { if (*p_vfile != NUL) ++msg_silent; else /* always scroll up, don't overwrite */ msg_scroll = TRUE; } /* * Like verbose_leave() and set cmdline_row when displaying the message. */ void verbose_leave_scroll(void) { if (*p_vfile != NUL) { if (--msg_silent < 0) msg_silent = 0; } else cmdline_row = msg_row; } /* * Called when 'verbosefile' is set: stop writing to the file. */ void verbose_stop(void) { if (verbose_fd != NULL) { fclose(verbose_fd); verbose_fd = NULL; } verbose_did_open = FALSE; } /* * Open the file 'verbosefile'. * Return FAIL or OK. */ int verbose_open(void) { if (verbose_fd == NULL && !verbose_did_open) { /* Only give the error message once. */ verbose_did_open = TRUE; verbose_fd = os_fopen((char *)p_vfile, "a"); if (verbose_fd == NULL) { EMSG2(_(e_notopen), p_vfile); return FAIL; } } return OK; } /* * Give a warning message (for searching). * Use 'w' highlighting and may repeat the message after redrawing */ void give_warning(char_u *message, bool hl) FUNC_ATTR_NONNULL_ARG(1) { // Don't do this for ":silent". if (msg_silent != 0) { return; } // Don't want a hit-enter prompt here. no_wait_return++; set_vim_var_string(VV_WARNINGMSG, (char *)message, -1); XFREE_CLEAR(keep_msg); if (hl) { keep_msg_attr = HL_ATTR(HLF_W); } else { keep_msg_attr = 0; } if (msg_ext_kind == NULL) { msg_ext_set_kind("wmsg"); } if (msg_attr((const char *)message, keep_msg_attr) && msg_scrolled == 0) { set_keep_msg(message, keep_msg_attr); } msg_didout = false; // Overwrite this message. msg_nowait = true; // Don't wait for this message. msg_col = 0; no_wait_return--; } void give_warning2(char_u *const message, char_u *const a1, bool hl) { vim_snprintf((char *)IObuff, IOSIZE, (char *)message, a1); give_warning(IObuff, hl); } /* * Advance msg cursor to column "col". */ void msg_advance(int col) { if (msg_silent != 0) { /* nothing to advance to */ msg_col = col; /* for redirection, may fill it up later */ return; } if (ui_has(kUIMessages)) { // TODO(bfredl): use byte count as a basic proxy. // later on we might add proper support for formatted messages. while (msg_ext_cur_len < (size_t)col) { msg_putchar(' '); } return; } if (col >= Columns) /* not enough room */ col = Columns - 1; if (cmdmsg_rl) while (msg_col > Columns - col) msg_putchar(' '); else while (msg_col < col) msg_putchar(' '); } /* * Used for "confirm()" function, and the :confirm command prefix. * Versions which haven't got flexible dialogs yet, and console * versions, get this generic handler which uses the command line. * * type = one of: * VIM_QUESTION, VIM_INFO, VIM_WARNING, VIM_ERROR or VIM_GENERIC * title = title string (can be NULL for default) * (neither used in console dialogs at the moment) * * Format of the "buttons" string: * "Button1Name\nButton2Name\nButton3Name" * The first button should normally be the default/accept * The second button should be the 'Cancel' button * Other buttons- use your imagination! * A '&' in a button name becomes a shortcut, so each '&' should be before a * different letter. */ int do_dialog ( int type, char_u *title, char_u *message, char_u *buttons, int dfltbutton, char_u *textfield, /* IObuff for inputdialog(), NULL otherwise */ int ex_cmd /* when TRUE pressing : accepts default and starts Ex command */ ) { int retval = 0; char_u *hotkeys; int c; int i; if (silent_mode // No dialogs in silent mode ("ex -s") || !ui_active() // Without a UI Nvim waits for input forever. ) { return dfltbutton; // return default option } int save_msg_silent = msg_silent; int oldState = State; msg_silent = 0; // If dialog prompts for input, user needs to see it! #8788 State = CONFIRM; setmouse(); /* * Since we wait for a keypress, don't make the * user press RETURN as well afterwards. */ ++no_wait_return; hotkeys = msg_show_console_dialog(message, buttons, dfltbutton); for (;; ) { // Get a typed character directly from the user. c = get_keystroke(NULL); switch (c) { case CAR: /* User accepts default option */ case NL: retval = dfltbutton; break; case Ctrl_C: /* User aborts/cancels */ case ESC: retval = 0; break; default: /* Could be a hotkey? */ if (c < 0) { /* special keys are ignored here */ continue; } if (c == ':' && ex_cmd) { retval = dfltbutton; ins_char_typebuf(':'); break; } // Make the character lowercase, as chars in "hotkeys" are. c = mb_tolower(c); retval = 1; for (i = 0; hotkeys[i]; i++) { if (utf_ptr2char(hotkeys + i) == c) { break; } i += utfc_ptr2len(hotkeys + i) - 1; retval++; } if (hotkeys[i]) break; /* No hotkey match, so keep waiting */ continue; } break; } xfree(hotkeys); msg_silent = save_msg_silent; State = oldState; setmouse(); --no_wait_return; msg_end_prompt(); return retval; } /* * Copy one character from "*from" to "*to", taking care of multi-byte * characters. Return the length of the character in bytes. */ static int copy_char ( char_u *from, char_u *to, int lowercase /* make character lower case */ ) { if (lowercase) { int c = mb_tolower(utf_ptr2char(from)); return utf_char2bytes(c, to); } int len = utfc_ptr2len(from); memmove(to, from, (size_t)len); return len; } #define HAS_HOTKEY_LEN 30 #define HOTK_LEN (has_mbyte ? MB_MAXBYTES : 1) /// Allocates memory for dialog string & for storing hotkeys /// /// Finds the size of memory required for the confirm_msg & for storing hotkeys /// and then allocates the memory for them. /// has_hotkey array is also filled-up. /// /// @param message Message which will be part of the confirm_msg /// @param buttons String containing button names /// @param[out] has_hotkey An element in this array is set to true if /// corresponding button has a hotkey /// /// @return Pointer to memory allocated for storing hotkeys static char_u * console_dialog_alloc(const char_u *message, char_u *buttons, bool has_hotkey[]) { int lenhotkey = HOTK_LEN; // count first button has_hotkey[0] = false; // Compute the size of memory to allocate. int len = 0; int idx = 0; char_u *r = buttons; while (*r) { if (*r == DLG_BUTTON_SEP) { len += 3; // '\n' -> ', '; 'x' -> '(x)' lenhotkey += HOTK_LEN; // each button needs a hotkey if (idx < HAS_HOTKEY_LEN - 1) { has_hotkey[++idx] = false; } } else if (*r == DLG_HOTKEY_CHAR) { r++; len++; // '&a' -> '[a]' if (idx < HAS_HOTKEY_LEN - 1) { has_hotkey[idx] = true; } } // Advance to the next character MB_PTR_ADV(r); } len += (int)(STRLEN(message) + 2 // for the NL's + STRLEN(buttons) + 3); // for the ": " and NUL lenhotkey++; // for the NUL // If no hotkey is specified, first char is used. if (!has_hotkey[0]) { len += 2; // "x" -> "[x]" } // Now allocate space for the strings xfree(confirm_msg); confirm_msg = xmalloc(len); *confirm_msg = NUL; return xmalloc(lenhotkey); } /* * Format the dialog string, and display it at the bottom of * the screen. Return a string of hotkey chars (if defined) for * each 'button'. If a button has no hotkey defined, the first character of * the button is used. * The hotkeys can be multi-byte characters, but without combining chars. * * Returns an allocated string with hotkeys. */ static char_u *msg_show_console_dialog(char_u *message, char_u *buttons, int dfltbutton) FUNC_ATTR_NONNULL_RET { bool has_hotkey[HAS_HOTKEY_LEN] = {false}; char_u *hotk = console_dialog_alloc(message, buttons, has_hotkey); copy_hotkeys_and_msg(message, buttons, dfltbutton, has_hotkey, hotk); display_confirm_msg(); return hotk; } /// Copies hotkeys & dialog message into the memory allocated for it /// /// @param message Message which will be part of the confirm_msg /// @param buttons String containing button names /// @param default_button_idx Number of default button /// @param has_hotkey An element in this array is true if corresponding button /// has a hotkey /// @param[out] hotkeys_ptr Pointer to the memory location where hotkeys will be copied static void copy_hotkeys_and_msg(const char_u *message, char_u *buttons, int default_button_idx, const bool has_hotkey[], char_u *hotkeys_ptr) { *confirm_msg = '\n'; STRCPY(confirm_msg + 1, message); char_u *msgp = confirm_msg + 1 + STRLEN(message); // Define first default hotkey. Keep the hotkey string NUL // terminated to avoid reading past the end. hotkeys_ptr[copy_char(buttons, hotkeys_ptr, TRUE)] = NUL; // Remember where the choices start, displaying starts here when // "hotkeys_ptr" typed at the more prompt. confirm_msg_tail = msgp; *msgp++ = '\n'; bool first_hotkey = false; // Is the first char of button a hotkey if (!has_hotkey[0]) { first_hotkey = true; // If no hotkey is specified, first char is used } int idx = 0; char_u *r = buttons; while (*r) { if (*r == DLG_BUTTON_SEP) { *msgp++ = ','; *msgp++ = ' '; // '\n' -> ', ' // Advance to next hotkey and set default hotkey hotkeys_ptr += (has_mbyte) ? STRLEN(hotkeys_ptr): 1; hotkeys_ptr[copy_char(r + 1, hotkeys_ptr, TRUE)] = NUL; if (default_button_idx) { default_button_idx--; } // If no hotkey is specified, first char is used. if (idx < HAS_HOTKEY_LEN - 1 && !has_hotkey[++idx]) { first_hotkey = true; } } else if (*r == DLG_HOTKEY_CHAR || first_hotkey) { if (*r == DLG_HOTKEY_CHAR) { ++r; } first_hotkey = false; if (*r == DLG_HOTKEY_CHAR) { // '&&a' -> '&a' *msgp++ = *r; } else { // '&a' -> '[a]' *msgp++ = (default_button_idx == 1) ? '[' : '('; msgp += copy_char(r, msgp, FALSE); *msgp++ = (default_button_idx == 1) ? ']' : ')'; // redefine hotkey hotkeys_ptr[copy_char(r, hotkeys_ptr, TRUE)] = NUL; } } else { // everything else copy literally msgp += copy_char(r, msgp, FALSE); } // advance to the next character MB_PTR_ADV(r); } *msgp++ = ':'; *msgp++ = ' '; *msgp = NUL; } /* * Display the ":confirm" message. Also called when screen resized. */ void display_confirm_msg(void) { // Avoid that 'q' at the more prompt truncates the message here. confirm_msg_used++; if (confirm_msg != NULL) { msg_ext_set_kind("confirm"); msg_puts_attr((const char *)confirm_msg, HL_ATTR(HLF_M)); } confirm_msg_used--; } int vim_dialog_yesno(int type, char_u *title, char_u *message, int dflt) { if (do_dialog(type, title == NULL ? (char_u *)_("Question") : title, message, (char_u *)_("&Yes\n&No"), dflt, NULL, FALSE) == 1) return VIM_YES; return VIM_NO; } int vim_dialog_yesnocancel(int type, char_u *title, char_u *message, int dflt) { switch (do_dialog(type, title == NULL ? (char_u *)_("Question") : title, message, (char_u *)_("&Yes\n&No\n&Cancel"), dflt, NULL, FALSE)) { case 1: return VIM_YES; case 2: return VIM_NO; } return VIM_CANCEL; } int vim_dialog_yesnoallcancel(int type, char_u *title, char_u *message, int dflt) { switch (do_dialog(type, title == NULL ? (char_u *)"Question" : title, message, (char_u *)_("&Yes\n&No\nSave &All\n&Discard All\n&Cancel"), dflt, NULL, FALSE)) { case 1: return VIM_YES; case 2: return VIM_NO; case 3: return VIM_ALL; case 4: return VIM_DISCARDALL; } return VIM_CANCEL; }