changeset 29900:a6721cafbc74 v9.0.0288

patch 9.0.0288: when 'cmdheight' is zero some messages are not displayed Commit: https://github.com/vim/vim/commit/9198de3ae2bd20ac51d580c44f2b43c282c1e773 Author: Bram Moolenaar <Bram@vim.org> Date: Sat Aug 27 21:30:03 2022 +0100 patch 9.0.0288: when 'cmdheight' is zero some messages are not displayed Problem: When 'cmdheight' is zero some messages are not displayed. Solution: Use a popup notification window.
author Bram Moolenaar <Bram@vim.org>
date Sat, 27 Aug 2022 22:45:03 +0200
parents d9726370e577
children 5387c6c0d6c8
files runtime/doc/options.txt src/feature.h src/message.c src/popupwin.c src/proto/message.pro src/proto/popupwin.pro src/proto/time.pro src/screen.c src/structs.h src/testdir/test_messages.vim src/time.c src/version.c
diffstat 12 files changed, 383 insertions(+), 50 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -1789,9 +1789,11 @@ A jump table for the options with a shor
 	page can have a different value.
 
 	When 'cmdheight' is zero, there is no command-line unless it is being
-	used.  Some informative messages will not be displayed, any other
-	messages will cause the |hit-enter| prompt.  Expect some other
-	unexpected behavior too.
+	used.  Informative messages will be displayed in a popup notification
+	window at the bottom if the window, using the MessageWindow highlight
+	group {only if compiled with the +popupwin and +timers features},
+	otherwise they will not be displayed.  Other messages will cause the
+	|hit-enter| prompt.  Expect some other unexpected behavior too.
 
 						*'cmdwinheight'* *'cwh'*
 'cmdwinheight' 'cwh'	number	(default 7)
--- a/src/feature.h
+++ b/src/feature.h
@@ -1064,6 +1064,13 @@
 # define FEAT_PROP_POPUP
 #endif
 
+/*
+ * +message_window	use a popup for messages when 'cmdheight' is zero
+ */
+#if defined(FEAT_PROP_POPUP) && defined(FEAT_TIMERS)
+# define HAS_MESSAGE_WINDOW
+#endif
+
 #if defined(FEAT_SYN_HL) && defined(FEAT_RELTIME)
 // Can limit syntax highlight time to 'redrawtime'.
 # define SYN_TIME_LIMIT 1
--- a/src/message.c
+++ b/src/message.c
@@ -954,8 +954,8 @@ msg_may_trunc(int force, char_u *s)
     int		n;
     int		room;
 
-    // If something unexpected happened "room" may be negative, check for that
-    // just in case.
+    // If 'cmdheight' is zero or something unexpected happened "room" may be
+    // negative.
     room = (int)(Rows - cmdline_row - 1) * Columns + sc_col - 1;
     if (room > 0 && (force || (shortmess(SHM_TRUNC) && !exmode_active))
 	    && (n = (int)STRLEN(s) - room) > 0 && p_ch > 0)
@@ -1421,6 +1421,21 @@ set_keep_msg_from_hist(void)
 #endif
 
 /*
+ * Return TRUE when the message popup window should be used.
+ */
+    int
+use_message_window(void)
+{
+#ifdef HAS_MESSAGE_WINDOW
+    // TRUE if there is no command line showing ('cmdheight' is zero and not
+    // already editing or showing a message) use a popup window for messages.
+    return p_ch == 0 && cmdline_row >= Rows;
+#else
+    return FALSE;
+#endif
+}
+
+/*
  * Prepare for outputting characters in the command line.
  */
     void
@@ -1444,7 +1459,20 @@ msg_start(void)
     }
 #endif
 
-    if (!msg_scroll && full_screen)	// overwrite last message
+    if (use_message_window())
+    {
+	if (popup_message_win_visible() && msg_col > 0)
+	{
+	    win_T *wp = popup_get_message_win();
+
+	    curbuf = wp->w_buffer;
+	    ml_append(wp->w_buffer->b_ml.ml_line_count,
+					      (char_u *)"", (colnr_T)0, FALSE);
+	    curbuf = curwin->w_buffer;
+	}
+	msg_col = 0;
+    }
+    else if (!msg_scroll && full_screen)	// overwrite last message
     {
 	msg_row = cmdline_row;
 	msg_col =
@@ -2195,6 +2223,46 @@ msg_puts_attr_len(char *str, int maxlen,
     need_fileinfo = FALSE;
 }
 
+// values for "where"
+#define PUT_APPEND 0		// append to "lnum"
+#define PUT_TRUNC 1		// replace "lnum"
+#define PUT_BELOW 2		// add below "lnum"
+				//
+#ifdef HAS_MESSAGE_WINDOW
+
+/*
+ * Put text "t_s" until "s" in the message window.
+ * "where" specifies where to put the text.
+ */
+    static void
+put_msg_win(win_T *wp, int where, char_u *t_s, char_u *end, linenr_T lnum)
+{
+    int	    c = *end;
+
+    *end = NUL;
+    if (where == PUT_BELOW)
+	ml_append_buf(wp->w_buffer, lnum, t_s, (colnr_T)0, FALSE);
+    else
+    {
+	char_u *newp;
+
+	curbuf = wp->w_buffer;
+	if (where == PUT_APPEND)
+	    newp = concat_str(ml_get(lnum), t_s);
+	else
+	    newp = vim_strnsave(t_s, end - t_s);
+	ml_replace(lnum, newp, FALSE);
+	curbuf = curwin->w_buffer;
+    }
+    redraw_win_later(wp, UPD_NOT_VALID);
+
+    // set msg_col so that a newline is written if needed
+    msg_col = STRLEN(t_s);
+
+    *end = c;
+}
+#endif
+
 /*
  * The display part of msg_puts_attr_len().
  * May be called recursively to display scroll-back text.
@@ -2215,6 +2283,43 @@ msg_puts_display(
     int		sb_col = msg_col;
     int		wrap;
     int		did_last_char;
+    int		where = PUT_APPEND;
+#ifdef HAS_MESSAGE_WINDOW
+    win_T	*msg_win = NULL;
+    linenr_T    lnum = 1;
+
+    if (use_message_window())
+    {
+	msg_win = popup_get_message_win();
+
+	if (msg_win != NULL)
+	{
+	    if (!popup_message_win_visible())
+	    {
+		if (*str == NL)
+		{
+		    // When not showing the message window and the output
+		    // starts with a NL show the message normally.
+		    msg_win = NULL;
+		}
+		else
+		{
+		    // currently hidden, make it empty
+		    curbuf = msg_win->w_buffer;
+		    while ((curbuf->b_ml.ml_flags & ML_EMPTY) == 0)
+			ml_delete(1);
+		    curbuf = curwin->w_buffer;
+		}
+	    }
+	    else
+	    {
+		lnum = msg_win->w_buffer->b_ml.ml_line_count;
+		if (msg_col == 0)
+		    where = PUT_TRUNC;
+	    }
+	}
+    }
+#endif
 
     did_wait_return = FALSE;
     while ((maxlen < 0 || (int)(s - str) < maxlen) && *s != NUL)
@@ -2244,15 +2349,29 @@ msg_puts_display(
 	     * ourselves).
 	     */
 	    if (t_col > 0)
+	    {
 		// output postponed text
-		t_puts(&t_col, t_s, s, attr);
+#ifdef HAS_MESSAGE_WINDOW
+		if (msg_win != NULL)
+		{
+		    put_msg_win(msg_win, where, t_s, s, lnum);
+		    t_col = 0;
+		    where = PUT_BELOW;
+		}
+		else
+#endif
+		    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.
-	    msg_scroll_up();
+#ifdef HAS_MESSAGE_WINDOW
+	    if (msg_win == NULL)
+#endif
+		// Scroll the screen up one line.
+		msg_scroll_up();
 
 	    msg_row = Rows - 2;
 	    if (msg_col >= Columns)	// can happen after screen resize
@@ -2285,18 +2404,25 @@ msg_puts_display(
 		// store text for scrolling back
 		store_sb_text(&sb_str, 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;
+#ifdef HAS_MESSAGE_WINDOW
+	    if (msg_win == NULL)
+	    {
+#endif
+		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;
+#ifdef HAS_MESSAGE_WINDOW
+	    }
+#endif
 	    if (p_more && lines_left == 0 && State != MODE_HITRETURN
 					    && !msg_no_more && !exmode_active)
 	    {
@@ -2322,8 +2448,19 @@ msg_puts_display(
 					    && 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);
+#ifdef HAS_MESSAGE_WINDOW
+	    if (msg_win != NULL)
+	    {
+		put_msg_win(msg_win, where, t_s, s, lnum);
+		t_col = 0;
+		where = PUT_BELOW;
+	    }
+	    else
+#endif
+		t_puts(&t_col, t_s, s, attr);
+	}
 
 	if (wrap && p_more && !recurse)
 	    // store text for scrolling back
@@ -2331,7 +2468,20 @@ msg_puts_display(
 
 	if (*s == '\n')		    // go to next line
 	{
-	    msg_didout = FALSE;	    // remember that line is empty
+#ifdef HAS_MESSAGE_WINDOW
+	    if (msg_win != NULL)
+	    {
+		// Ignore a NL when the buffer is empty, it is used to scroll
+		// up the text.
+		if ((msg_win->w_buffer->b_ml.ml_flags & ML_EMPTY) == 0)
+		{
+		    put_msg_win(msg_win, PUT_BELOW, t_s, t_s, lnum);
+		    ++lnum;
+		}
+	    }
+	    else
+#endif
+		msg_didout = FALSE;	    // remember that line is empty
 #ifdef FEAT_RIGHTLEFT
 	    if (cmdmsg_rl)
 		msg_col = Columns - 1;
@@ -2344,6 +2494,7 @@ msg_puts_display(
 	else if (*s == '\r')	    // go to column 0
 	{
 	    msg_col = 0;
+	    where = PUT_TRUNC;
 	}
 	else if (*s == '\b')	    // go to previous char
 	{
@@ -2352,9 +2503,14 @@ msg_puts_display(
 	}
 	else if (*s == TAB)	    // translate Tab into spaces
 	{
-	    do
-		msg_screen_putchar(' ', attr);
-	    while (msg_col & 7);
+#ifdef HAS_MESSAGE_WINDOW
+	    if (msg_win != NULL)
+		msg_col = (msg_col + 7) % 8;
+	    else
+#endif
+		do
+		    msg_screen_putchar(' ', attr);
+		while (msg_col & 7);
 	}
 	else if (*s == BELL)		// beep (from ":sh")
 	    vim_beep(BO_SH);
@@ -2403,7 +2559,19 @@ msg_puts_display(
 
     // output any postponed text
     if (t_col > 0)
-	t_puts(&t_col, t_s, s, attr);
+    {
+#ifdef HAS_MESSAGE_WINDOW
+	if (msg_win != NULL)
+	    put_msg_win(msg_win, where, t_s, s, lnum);
+	else
+#endif
+	    t_puts(&t_col, t_s, s, attr);
+    }
+
+#ifdef HAS_MESSAGE_WINDOW
+    if (msg_win != NULL)
+	popup_show_message_win();
+#endif
     if (p_more && !recurse)
 	store_sb_text(&sb_str, s, attr, &sb_col, FALSE);
 
@@ -2431,6 +2599,10 @@ message_filtered(char_u *msg)
     static void
 msg_scroll_up(void)
 {
+#ifdef HAS_MESSAGE_WINDOW
+    if (use_message_window())
+	return;
+#endif
 #ifdef FEAT_GUI
     // Remove the cursor before scrolling, ScreenLines[] is going
     // to become invalid.
--- a/src/popupwin.c
+++ b/src/popupwin.c
@@ -412,15 +412,20 @@ popup_handle_scrollbar_click(win_T *wp, 
 }
 
 #if defined(FEAT_TIMERS)
+/*
+ * Add a timer to "wp" with "time".
+ * If "close" is true use popup_close(), otherwise popup_hide().
+ */
     static void
-popup_add_timeout(win_T *wp, int time)
+popup_add_timeout(win_T *wp, int time, int close)
 {
     char_u	    cbbuf[50];
     char_u	    *ptr = cbbuf;
     typval_T	    tv;
 
     vim_snprintf((char *)cbbuf, sizeof(cbbuf),
-				       "(_) => popup_close(%d)", wp->w_id);
+		close ? "(_) => popup_close(%d)" : "(_) => popup_hide(%d)",
+		wp->w_id);
     if (get_lambda_tv_and_compile(&ptr, &tv, FALSE, &EVALARG_EVALUATE) == OK)
     {
 	wp->w_popup_timer = create_timer(time, 0);
@@ -669,7 +674,8 @@ popup_highlight_curline(win_T *wp)
 
 	    if (syn_name2id((char_u *)linehl) == 0)
 		linehl = "PmenuSel";
-	    sign_define_by_name(sign_name, NULL, (char_u *)linehl, NULL, NULL, NULL, NULL);
+	    sign_define_by_name(sign_name, NULL, (char_u *)linehl,
+						       NULL, NULL, NULL, NULL);
 	}
 
 	sign_place(&sign_id, (char_u *)"PopUpMenu", sign_name,
@@ -905,7 +911,7 @@ apply_general_options(win_T *wp, dict_T 
     // Add timer to close the popup after some time.
     nr = dict_get_number(dict, "time");
     if (nr > 0)
-	popup_add_timeout(wp, nr);
+	popup_add_timeout(wp, nr, TRUE);
 #endif
 
     di = dict_find(dict, (char_u *)"moved", -1);
@@ -1289,6 +1295,9 @@ popup_adjust_position(win_T *wp)
 	    if (wp->w_winrow >= Rows)
 		wp->w_winrow = Rows - 1;
 	}
+	if (wp->w_popup_pos == POPPOS_BOTTOM)
+	    // assume that each buffer line takes one screen line
+	    wp->w_winrow = MAX(Rows - wp->w_buffer->b_ml.ml_line_count - 1, 0);
 
 	if (!use_wantcol)
 	    center_hor = TRUE;
@@ -1649,6 +1658,7 @@ typedef enum
     TYPE_ATCURSOR,
     TYPE_BEVAL,
     TYPE_NOTIFICATION,
+    TYPE_MESSAGE_WIN,	// similar to TYPE_NOTIFICATION
     TYPE_DIALOG,
     TYPE_MENU,
     TYPE_PREVIEW,	// preview window
@@ -1656,6 +1666,15 @@ typedef enum
 } create_type_T;
 
 /*
+ * Return TRUE if "type" is TYPE_NOTIFICATION or TYPE_MESSAGE_WIN.
+ */
+    static int
+popup_is_notification(create_type_T type)
+{
+    return type == TYPE_NOTIFICATION || type == TYPE_MESSAGE_WIN;
+}
+
+/*
  * Make "buf" empty and set the contents to "text".
  * Used by popup_create() and popup_settext().
  */
@@ -1914,6 +1933,21 @@ popup_terminal_exists(void)
 #endif
 
 /*
+ * Set the color for a notification window.
+ */
+    static void
+popup_update_color(win_T *wp, create_type_T type)
+{
+    char    *hiname = type == TYPE_MESSAGE_WIN
+				       ? "MessageWindow" : "PopupNotification";
+    int		nr = syn_name2id((char_u *)hiname);
+
+    set_string_option_direct_in_win(wp, (char_u *)"wincolor", -1,
+		(char_u *)(nr == 0 ? "WarningMsg" : hiname),
+		OPT_FREE|OPT_LOCAL, 0);
+}
+
+/*
  * popup_create({text}, {options})
  * popup_atcursor({text}, {options})
  * etc.
@@ -1928,7 +1962,6 @@ popup_create(typval_T *argvars, typval_T
     int		new_buffer;
     buf_T	*buf = NULL;
     dict_T	*d = NULL;
-    int		nr;
     int		i;
 
     if (argvars != NULL)
@@ -1975,7 +2008,7 @@ popup_create(typval_T *argvars, typval_T
     {
 	if (dict_has_key(d, "tabpage"))
 	    tabnr = (int)dict_get_number(d, "tabpage");
-	else if (type == TYPE_NOTIFICATION)
+	else if (popup_is_notification(type))
 	    tabnr = -1;  // notifications are global by default
 	else
 	    tabnr = 0;
@@ -2101,7 +2134,7 @@ popup_create(typval_T *argvars, typval_T
     wp->w_zindex = POPUPWIN_DEFAULT_ZINDEX;
     wp->w_popup_close = POPCLOSE_NONE;
 
-    if (type == TYPE_NOTIFICATION)
+    if (popup_is_notification(type))
     {
 	win_T  *twp, *nextwin;
 	int	height = buf->b_ml.ml_line_count + 3;
@@ -2140,10 +2173,7 @@ popup_create(typval_T *argvars, typval_T
 	wp->w_popup_padding[1] = 1;
 	wp->w_popup_padding[3] = 1;
 
-	nr = syn_name2id((char_u *)"PopupNotification");
-	set_string_option_direct_in_win(wp, (char_u *)"wincolor", -1,
-		(char_u *)(nr == 0 ? "WarningMsg" : "PopupNotification"),
-		OPT_FREE|OPT_LOCAL, 0);
+	popup_update_color(wp, type);
     }
 
     if (type == TYPE_DIALOG || type == TYPE_MENU)
@@ -2203,8 +2233,8 @@ popup_create(typval_T *argvars, typval_T
 	apply_options(wp, d, TRUE);
 
 #ifdef FEAT_TIMERS
-    if (type == TYPE_NOTIFICATION && wp->w_popup_timer == NULL)
-	popup_add_timeout(wp, 3000);
+    if (popup_is_notification(type) && wp->w_popup_timer == NULL)
+	popup_add_timeout(wp, 3000, type == TYPE_NOTIFICATION);
 #endif
 
     popup_adjust_position(wp);
@@ -4408,6 +4438,86 @@ popup_close_info(void)
 }
 #endif
 
+#if defined(HAS_MESSAGE_WINDOW) || defined(PROTO)
+
+// Window used for messages when 'winheight' is zero.
+static win_T *message_win = NULL;
+
+/*
+ * Get the message window.
+ * Returns NULL if something failed.
+ */
+    win_T *
+popup_get_message_win(void)
+{
+    if (message_win == NULL)
+    {
+	int i;
+
+	message_win = popup_create(NULL, NULL, TYPE_MESSAGE_WIN);
+
+	if (message_win == NULL)
+	    return NULL;
+
+	// use the full screen width
+	message_win->w_width = Columns;
+
+	// position at bottom of screen
+	message_win->w_popup_pos = POPPOS_BOTTOM;
+	message_win->w_wantcol = 1;
+	message_win->w_minwidth = 9999;
+
+	// no padding, border at the top
+	for (i = 0; i < 4; ++i)
+	    message_win->w_popup_padding[i] = 0;
+	for (i = 1; i < 4; ++i)
+	    message_win->w_popup_border[i] = 0;
+
+	if (message_win->w_popup_timer != NULL)
+	    message_win->w_popup_timer->tr_keep = TRUE;
+    }
+    return message_win;
+}
+
+/*
+ * If the message window is not visible: show it
+ * If the message window is visible: reset the timeout
+ */
+    void
+popup_show_message_win(void)
+{
+    if (message_win != NULL)
+    {
+	if ((message_win->w_popup_flags & POPF_HIDDEN) != 0)
+	{
+	    // the highlight may have changed.
+	    popup_update_color(message_win, TYPE_MESSAGE_WIN);
+	    popup_show(message_win);
+	}
+	else if (message_win->w_popup_timer != NULL)
+	    timer_start(message_win->w_popup_timer);
+    }
+}
+
+    int
+popup_message_win_visible(void)
+{
+    return message_win != NULL
+	&& (message_win->w_popup_flags & POPF_HIDDEN) == 0;
+}
+
+/*
+ * If the message window is visible: hide it.
+ */
+    void
+popup_hide_message_win(void)
+{
+    if (message_win != NULL)
+	popup_hide(message_win);
+}
+
+#endif
+
 /*
  * Close any popup for a text property associated with "win".
  * Return TRUE if a popup was closed.
--- a/src/proto/message.pro
+++ b/src/proto/message.pro
@@ -23,6 +23,7 @@ void msg_end_prompt(void);
 void wait_return(int redraw);
 void set_keep_msg(char_u *s, int attr);
 void set_keep_msg_from_hist(void);
+int use_message_window(void);
 void msg_start(void);
 void msg_starthere(void);
 void msg_putchar(int c);
--- a/src/proto/popupwin.pro
+++ b/src/proto/popupwin.pro
@@ -62,6 +62,10 @@ int popup_create_preview_window(int info
 void popup_close_preview(void);
 void popup_hide_info(void);
 void popup_close_info(void);
+win_T *popup_get_message_win(void);
+void popup_show_message_win(void);
+int popup_message_win_visible(void);
+void popup_hide_message_win(void);
 int popup_win_closed(win_T *win);
 void popup_set_title(win_T *wp);
 void popup_update_preview_title(void);
--- a/src/proto/time.pro
+++ b/src/proto/time.pro
@@ -9,6 +9,7 @@ void f_strftime(typval_T *argvars, typva
 void f_strptime(typval_T *argvars, typval_T *rettv);
 long proftime_time_left(proftime_T *due, proftime_T *now);
 timer_T *create_timer(long msec, int repeat);
+void timer_start(timer_T *timer);
 long check_due_timer(void);
 void stop_timer(timer_T *timer);
 int set_ref_in_timer(int copyID);
--- a/src/screen.c
+++ b/src/screen.c
@@ -4211,10 +4211,10 @@ showmode(void)
     int		nwr_save;
     int		sub_attr;
 
-    do_mode = ((p_smd && msg_silent == 0)
+    do_mode = p_smd && msg_silent == 0 && p_ch > 0
 	    && ((State & MODE_INSERT)
 		|| restart_edit != NUL
-		|| VIsual_active));
+		|| VIsual_active);
     if (do_mode || reg_recording != 0)
     {
 	if (skip_showmode())
--- a/src/structs.h
+++ b/src/structs.h
@@ -2569,6 +2569,7 @@ struct timer_S
     proftime_T	tr_due;		    // when the callback is to be invoked
     char	tr_firing;	    // when TRUE callback is being called
     char	tr_paused;	    // when TRUE callback is not invoked
+    char	tr_keep;	    // when TRUE keep timer after it fired
     int		tr_repeat;	    // number of times to repeat, -1 forever
     long	tr_interval;	    // msec
     callback_T	tr_callback;
@@ -2605,6 +2606,7 @@ typedef enum {
     POPPOS_BOTRIGHT,
     POPPOS_TOPRIGHT,
     POPPOS_CENTER,
+    POPPOS_BOTTOM,	// bottom of popup at bottom of screen
     POPPOS_NONE
 } poppos_T;
 
--- a/src/testdir/test_messages.vim
+++ b/src/testdir/test_messages.vim
@@ -392,18 +392,31 @@ func Test_cmdheight_zero()
   set cmdheight=0
   set showcmd
   redraw!
+  let using_popupwin = has('timers') && has('popupwin')
 
   echo 'test echo'
-  call assert_equal(116, screenchar(&lines, 1))
+  if using_popupwin
+    redraw
+    call assert_equal('test echo', Screenline(&lines))
+  else
+    call assert_equal(116, screenchar(&lines, 1))
+  endif
   redraw!
 
   echomsg 'test echomsg'
-  call assert_equal(116, screenchar(&lines, 1))
+  if using_popupwin
+    redraw
+    call assert_equal('test echomsg', Screenline(&lines))
+  else
+    call assert_equal(116, screenchar(&lines, 1))
+  endif
   redraw!
 
-  call feedkeys(":ls\<CR>", "xt")
-  call assert_equal(':ls', Screenline(&lines - 1))
-  redraw!
+  if !using_popupwin
+    call feedkeys(":ls\<CR>", "xt")
+    call assert_equal(':ls', Screenline(&lines))
+    redraw!
+  endif
 
   let char = getchar(0)
   call assert_match(char, 0)
@@ -420,6 +433,9 @@ func Test_cmdheight_zero()
   call assert_equal('otherstring', getline(1))
 
   call feedkeys("g\<C-g>", "xt")
+  if using_popupwin
+    redraw
+  endif
   call assert_match(
         \ 'Col 1 of 11; Line 1 of 1; Word 1 of 1',
         \ Screenline(&lines))
--- a/src/time.c
+++ b/src/time.c
@@ -464,11 +464,21 @@ create_timer(long msec, int repeat)
 	timer->tr_repeat = repeat - 1;
     timer->tr_interval = msec;
 
-    profile_setlimit(msec, &timer->tr_due);
+    timer_start(timer);
     return timer;
 }
 
 /*
+ * (Re)start a timer.
+ */
+    void
+timer_start(timer_T *timer)
+{
+    profile_setlimit(timer->tr_interval, &timer->tr_due);
+    timer->tr_paused = FALSE;
+}
+
+/*
  * Invoke the callback of "timer".
  */
     static void
@@ -603,8 +613,13 @@ check_due_timer(void)
 	    else
 	    {
 		this_due = -1;
-		remove_timer(timer);
-		free_timer(timer);
+		if (timer->tr_keep)
+		    timer->tr_paused = TRUE;
+		else
+		{
+		    remove_timer(timer);
+		    free_timer(timer);
+		}
 	    }
 	}
 	if (this_due > 0 && (next_due == -1 || next_due > this_due))
@@ -826,6 +841,7 @@ f_timer_pause(typval_T *argvars, typval_
     else
     {
 	int	paused = (int)tv_get_bool(&argvars[1]);
+
 	timer = find_timer((int)tv_get_number(&argvars[0]));
 	if (timer != NULL)
 	    timer->tr_paused = paused;
--- a/src/version.c
+++ b/src/version.c
@@ -708,6 +708,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    288,
+/**/
     287,
 /**/
     286,