patch 8.1.0027: difficult to make a plugin that feeds a line to a job

Problem:    Difficult to make a plugin that feeds a line to a job.
Solution:   Add the nitial code for the "prompt" buftype.
diff --git a/src/Makefile b/src/Makefile
index 0e4c555..ca97254 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -2252,6 +2252,7 @@
 	test_popup \
 	test_preview \
 	test_profile \
+	test_prompt_buffer \
 	test_put \
 	test_python2 \
 	test_python3 \
diff --git a/src/buffer.c b/src/buffer.c
index e3cbdac..1c55acb 100644
--- a/src/buffer.c
+++ b/src/buffer.c
@@ -851,6 +851,10 @@
 #ifdef FEAT_TERMINAL
     free_terminal(buf);
 #endif
+#ifdef FEAT_JOB_CHANNEL
+    vim_free(buf->b_prompt_text);
+    free_callback(buf->b_prompt_callback, buf->b_prompt_partial);
+#endif
 
     buf_hashtab_remove(buf);
 
@@ -5634,6 +5638,15 @@
 }
 
 /*
+ * Return TRUE if "buf" is a prompt buffer.
+ */
+    int
+bt_prompt(buf_T *buf)
+{
+    return buf != NULL && buf->b_p_bt[0] == 'p';
+}
+
+/*
  * Return TRUE if "buf" is a "nofile", "acwrite" or "terminal" buffer.
  * This means the buffer name is not a file name.
  */
@@ -5642,7 +5655,8 @@
 {
     return buf != NULL && ((buf->b_p_bt[0] == 'n' && buf->b_p_bt[2] == 'f')
 	    || buf->b_p_bt[0] == 'a'
-	    || buf->b_p_bt[0] == 't');
+	    || buf->b_p_bt[0] == 't'
+	    || buf->b_p_bt[0] == 'p');
 }
 
 /*
@@ -5651,7 +5665,9 @@
     int
 bt_dontwrite(buf_T *buf)
 {
-    return buf != NULL && (buf->b_p_bt[0] == 'n' || buf->b_p_bt[0] == 't');
+    return buf != NULL && (buf->b_p_bt[0] == 'n'
+	         || buf->b_p_bt[0] == 't'
+	         || buf->b_p_bt[0] == 'p');
 }
 
     int
diff --git a/src/channel.c b/src/channel.c
index 504d6b6..40a3e95 100644
--- a/src/channel.c
+++ b/src/channel.c
@@ -5836,4 +5836,38 @@
     return 1;
 }
 
+    void
+invoke_prompt_callback(void)
+{
+    typval_T	rettv;
+    int		dummy;
+    typval_T	argv[2];
+    char_u	*text;
+    char_u	*prompt;
+    linenr_T	lnum = curbuf->b_ml.ml_line_count;
+
+    // Add a new line for the prompt before invoking the callback, so that
+    // text can always be inserted above the last line.
+    ml_append(lnum, (char_u  *)"", 0, FALSE);
+    curwin->w_cursor.lnum = lnum + 1;
+    curwin->w_cursor.col = 0;
+
+    if (curbuf->b_prompt_callback == NULL)
+	return;
+    text = ml_get(lnum);
+    prompt = prompt_text();
+    if (STRLEN(text) >= STRLEN(prompt))
+	text += STRLEN(prompt);
+    argv[0].v_type = VAR_STRING;
+    argv[0].vval.v_string = vim_strsave(text);
+    argv[1].v_type = VAR_UNKNOWN;
+
+    call_func(curbuf->b_prompt_callback,
+	      (int)STRLEN(curbuf->b_prompt_callback),
+	      &rettv, 1, argv, NULL, 0L, 0L, &dummy, TRUE,
+	      curbuf->b_prompt_partial, NULL);
+    clear_tv(&argv[0]);
+    clear_tv(&rettv);
+}
+
 #endif /* FEAT_JOB_CHANNEL */
diff --git a/src/diff.c b/src/diff.c
index cc9e6de..c67654f 100644
--- a/src/diff.c
+++ b/src/diff.c
@@ -2141,6 +2141,13 @@
     exarg_T	ea;
     char_u	buf[30];
 
+#ifdef FEAT_JOB_CHANNEL
+    if (bt_prompt(curbuf))
+    {
+	vim_beep(BO_OPER);
+	return;
+    }
+#endif
     if (count == 0)
 	ea.arg = (char_u *)"";
     else
diff --git a/src/edit.c b/src/edit.c
index 1ae8e2d..1b79ecc 100644
--- a/src/edit.c
+++ b/src/edit.c
@@ -203,6 +203,9 @@
 
 static void ins_redraw(int ready);
 static void ins_ctrl_v(void);
+#ifdef FEAT_JOB_CHANNEL
+static void init_prompt(int cmdchar_todo);
+#endif
 static void undisplay_dollar(void);
 static void insert_special(int, int, int);
 static void internal_format(int textwidth, int second_indent, int flags, int format_only, int c);
@@ -351,6 +354,9 @@
     int		inserted_space = FALSE;     /* just inserted a space */
     int		replaceState = REPLACE;
     int		nomove = FALSE;		    /* don't move cursor on return */
+#ifdef FEAT_JOB_CHANNEL
+    int		cmdchar_todo = cmdchar;
+#endif
 
     /* Remember whether editing was restarted after CTRL-O. */
     did_restart_edit = restart_edit;
@@ -707,6 +713,14 @@
 	    foldCheckClose();
 #endif
 
+#ifdef FEAT_JOB_CHANNEL
+	if (bt_prompt(curbuf))
+	{
+	    init_prompt(cmdchar_todo);
+	    cmdchar_todo = NUL;
+	}
+#endif
+
 	/*
 	 * If we inserted a character at the last position of the last line in
 	 * the window, scroll the window one line up. This avoids an extra
@@ -1374,6 +1388,18 @@
 		goto doESCkey;
 	    }
 #endif
+#ifdef FEAT_JOB_CHANNEL
+	    if (bt_prompt(curbuf))
+	    {
+		buf_T *buf = curbuf;
+
+		invoke_prompt_callback();
+		if (curbuf != buf)
+		    // buffer changed, get out of Insert mode
+		    goto doESCkey;
+		break;
+	    }
+#endif
 	    if (ins_eol(c) == FAIL && !p_im)
 		goto doESCkey;	    /* out of memory */
 	    auto_format(FALSE, FALSE);
@@ -1808,6 +1834,58 @@
     }
 }
 
+#if defined(FEAT_JOB_CHANNEL) || defined(PROTO)
+/*
+ * Return the effective prompt for the current buffer.
+ */
+    char_u *
+prompt_text(void)
+{
+    if (curbuf->b_prompt_text == NULL)
+	return (char_u *)"% ";
+    return curbuf->b_prompt_text;
+}
+
+/*
+ * Prepare for prompt mode: Make sure the last line has the prompt text.
+ * Move the cursor to this line.
+ */
+    static void
+init_prompt(int cmdchar_todo)
+{
+    char_u *prompt = prompt_text();
+    char_u *text;
+
+    curwin->w_cursor.lnum = curbuf->b_ml.ml_line_count;
+    text = ml_get_curline();
+    if (STRNCMP(text, prompt, STRLEN(prompt)) != 0)
+    {
+	// prompt is missing, insert it or append a line with it
+	if (*text == NUL)
+	    ml_replace(curbuf->b_ml.ml_line_count, prompt, TRUE);
+	else
+	    ml_append(curbuf->b_ml.ml_line_count, prompt, 0, FALSE);
+	curwin->w_cursor.lnum = curbuf->b_ml.ml_line_count;
+	coladvance((colnr_T)MAXCOL);
+	changed_bytes(curbuf->b_ml.ml_line_count, 0);
+    }
+    if (cmdchar_todo == 'A')
+	coladvance((colnr_T)MAXCOL);
+    if (cmdchar_todo == 'I' || curwin->w_cursor.col <= (int)STRLEN(prompt))
+	curwin->w_cursor.col = STRLEN(prompt);
+}
+
+/*
+ * Return TRUE if the cursor is in the editable position of the prompt line.
+ */
+    int
+prompt_curpos_editable()
+{
+    return curwin->w_cursor.lnum == curbuf->b_ml.ml_line_count
+	&& curwin->w_cursor.col >= (int)STRLEN(prompt_text());
+}
+#endif
+
 /*
  * Undo the previous edit_putchar().
  */
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 90aa2ef..9441bd8 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -294,6 +294,10 @@
 #endif
 static void f_prevnonblank(typval_T *argvars, typval_T *rettv);
 static void f_printf(typval_T *argvars, typval_T *rettv);
+#ifdef FEAT_JOB_CHANNEL
+static void f_prompt_setcallback(typval_T *argvars, typval_T *rettv);
+static void f_prompt_setprompt(typval_T *argvars, typval_T *rettv);
+#endif
 static void f_pumvisible(typval_T *argvars, typval_T *rettv);
 #ifdef FEAT_PYTHON3
 static void f_py3eval(typval_T *argvars, typval_T *rettv);
@@ -744,6 +748,10 @@
 #endif
     {"prevnonblank",	1, 1, f_prevnonblank},
     {"printf",		1, 19, f_printf},
+#ifdef FEAT_JOB_CHANNEL
+    {"prompt_setcallback", 2, 2, f_prompt_setcallback},
+    {"prompt_setprompt", 2, 2, f_prompt_setprompt},
+#endif
     {"pumvisible",	0, 0, f_pumvisible},
 #ifdef FEAT_PYTHON3
     {"py3eval",		1, 1, f_py3eval},
@@ -1240,6 +1248,11 @@
 	appended_lines_mark(lnum, added);
 	if (curwin->w_cursor.lnum > lnum)
 	    curwin->w_cursor.lnum += added;
+#ifdef FEAT_JOB_CHANNEL
+	if (bt_prompt(curbuf) && (State & INSERT))
+	    // show the line with the prompt
+	    update_topline();
+#endif
     }
     else
 	rettv->vval.v_number = 1;	/* Failed */
@@ -8379,6 +8392,57 @@
     did_emsg |= saved_did_emsg;
 }
 
+#ifdef FEAT_JOB_CHANNEL
+/*
+ * "prompt_setcallback({buffer}, {callback})" function
+ */
+    static void
+f_prompt_setcallback(typval_T *argvars, typval_T *rettv UNUSED)
+{
+    buf_T	*buf;
+    char_u	*callback;
+    partial_T	*partial;
+
+    if (check_secure())
+	return;
+    buf = get_buf_tv(&argvars[0], FALSE);
+    if (buf == NULL)
+	return;
+
+    callback = get_callback(&argvars[1], &partial);
+    if (callback == NULL)
+	return;
+
+    free_callback(buf->b_prompt_callback, buf->b_prompt_partial);
+    if (partial == NULL)
+	buf->b_prompt_callback = vim_strsave(callback);
+    else
+	/* pointer into the partial */
+	buf->b_prompt_callback = callback;
+    buf->b_prompt_partial = partial;
+}
+
+/*
+ * "prompt_setprompt({buffer}, {text})" function
+ */
+    static void
+f_prompt_setprompt(typval_T *argvars, typval_T *rettv UNUSED)
+{
+    buf_T	*buf;
+    char_u	*text;
+
+    if (check_secure())
+	return;
+    buf = get_buf_tv(&argvars[0], FALSE);
+    if (buf == NULL)
+	return;
+
+    text = get_tv_string(&argvars[1]);
+    vim_free(buf->b_prompt_text);
+    buf->b_prompt_text = vim_strsave(text);
+}
+#endif
+
 /*
  * "pumvisible()" function
  */
diff --git a/src/normal.c b/src/normal.c
index 58f7a7a..a01a434 100644
--- a/src/normal.c
+++ b/src/normal.c
@@ -4180,6 +4180,11 @@
     static void
 nv_addsub(cmdarg_T *cap)
 {
+#ifdef FEAT_JOB_CHANNEL
+    if (bt_prompt(curbuf) && !prompt_curpos_editable())
+	clearopbeep(cap->oap);
+    else
+#endif
     if (!VIsual_active && cap->oap->op_type == OP_NOP)
     {
 	prep_redo_cmd(cap);
@@ -6214,6 +6219,17 @@
 	    cmdwin_result = CAR;
 	else
 #endif
+#ifdef FEAT_JOB_CHANNEL
+	/* In a prompt buffer a <CR> in the last line invokes the callback. */
+	if (bt_prompt(curbuf) && cap->cmdchar == CAR
+		       && curwin->w_cursor.lnum == curbuf->b_ml.ml_line_count)
+	{
+	    invoke_prompt_callback();
+	    if (restart_edit == 0)
+		restart_edit = 'a';
+	}
+	else
+#endif
 	{
 	    cap->oap->motion_type = MLINE;
 	    if (cursor_down(cap->count1, cap->oap->op_type == OP_NOP) == FAIL)
@@ -6972,6 +6988,13 @@
 {
     if (!checkclearopq(cap->oap))
     {
+#ifdef FEAT_JOB_CHANNEL
+	if (bt_prompt(curbuf))
+	{
+	    clearopbeep(cap->oap);
+	    return;
+	}
+#endif
 	u_undo((int)cap->count1);
 	curwin->w_set_curswant = TRUE;
     }
@@ -6989,6 +7012,13 @@
 
     if (checkclearop(cap->oap))
 	return;
+#ifdef FEAT_JOB_CHANNEL
+    if (bt_prompt(curbuf) && !prompt_curpos_editable())
+    {
+	clearopbeep(cap->oap);
+	return;
+    }
+#endif
 
     /* get another character */
     if (cap->nchar == Ctrl_V)
@@ -7465,6 +7495,13 @@
     if (term_swap_diff() == OK)
 	return;
 #endif
+#ifdef FEAT_JOB_CHANNEL
+    if (bt_prompt(curbuf) && !prompt_curpos_editable())
+    {
+	clearopbeep(cap->oap);
+	return;
+    }
+#endif
     if (VIsual_active)	/* "vs" and "vS" are the same as "vc" */
     {
 	if (cap->cmdchar == 'S')
@@ -8570,7 +8607,16 @@
 nv_tilde(cmdarg_T *cap)
 {
     if (!p_to && !VIsual_active && cap->oap->op_type != OP_TILDE)
+    {
+#ifdef FEAT_JOB_CHANNEL
+	if (bt_prompt(curbuf) && !prompt_curpos_editable())
+	{
+	    clearopbeep(cap->oap);
+	    return;
+	}
+#endif
 	n_swapchar(cap);
+    }
     else
 	nv_operator(cap);
 }
@@ -8585,6 +8631,13 @@
     int	    op_type;
 
     op_type = get_op_type(cap->cmdchar, cap->nchar);
+#ifdef FEAT_JOB_CHANNEL
+    if (bt_prompt(curbuf) && op_is_change(op_type) && !prompt_curpos_editable())
+    {
+	clearopbeep(cap->oap);
+	return;
+    }
+#endif
 
     if (op_type == cap->oap->op_type)	    /* double operator works on lines */
 	nv_lineop(cap);
@@ -9426,6 +9479,12 @@
 #endif
 	clearopbeep(cap->oap);
     }
+#ifdef FEAT_JOB_CHANNEL
+    else if (bt_prompt(curbuf) && !prompt_curpos_editable())
+    {
+	clearopbeep(cap->oap);
+    }
+#endif
     else
     {
 	dir = (cap->cmdchar == 'P'
@@ -9551,6 +9610,12 @@
 #endif
     if (VIsual_active)  /* switch start and end of visual */
 	v_swap_corners(cap->cmdchar);
+#ifdef FEAT_JOB_CHANNEL
+    else if (bt_prompt(curbuf))
+    {
+	clearopbeep(cap->oap);
+    }
+#endif
     else
 	n_opencmd(cap);
 }
diff --git a/src/ops.c b/src/ops.c
index 1bc51d8..65fc75e 100644
--- a/src/ops.c
+++ b/src/ops.c
@@ -126,43 +126,47 @@
 static int	fmt_check_par(linenr_T);
 #endif
 
+// Flags for third item in "opchars".
+#define OPF_LINES  1	// operator always works on lines
+#define OPF_CHANGE 2	// operator changes text
+
 /*
  * The names of operators.
  * IMPORTANT: Index must correspond with defines in vim.h!!!
- * The third field indicates whether the operator always works on lines.
+ * The third field holds OPF_ flags.
  */
 static char opchars[][3] =
 {
-    {NUL, NUL, FALSE},	/* OP_NOP */
-    {'d', NUL, FALSE},	/* OP_DELETE */
-    {'y', NUL, FALSE},	/* OP_YANK */
-    {'c', NUL, FALSE},	/* OP_CHANGE */
-    {'<', NUL, TRUE},	/* OP_LSHIFT */
-    {'>', NUL, TRUE},	/* OP_RSHIFT */
-    {'!', NUL, TRUE},	/* OP_FILTER */
-    {'g', '~', FALSE},	/* OP_TILDE */
-    {'=', NUL, TRUE},	/* OP_INDENT */
-    {'g', 'q', TRUE},	/* OP_FORMAT */
-    {':', NUL, TRUE},	/* OP_COLON */
-    {'g', 'U', FALSE},	/* OP_UPPER */
-    {'g', 'u', FALSE},	/* OP_LOWER */
-    {'J', NUL, TRUE},	/* DO_JOIN */
-    {'g', 'J', TRUE},	/* DO_JOIN_NS */
-    {'g', '?', FALSE},	/* OP_ROT13 */
-    {'r', NUL, FALSE},	/* OP_REPLACE */
-    {'I', NUL, FALSE},	/* OP_INSERT */
-    {'A', NUL, FALSE},	/* OP_APPEND */
-    {'z', 'f', TRUE},	/* OP_FOLD */
-    {'z', 'o', TRUE},	/* OP_FOLDOPEN */
-    {'z', 'O', TRUE},	/* OP_FOLDOPENREC */
-    {'z', 'c', TRUE},	/* OP_FOLDCLOSE */
-    {'z', 'C', TRUE},	/* OP_FOLDCLOSEREC */
-    {'z', 'd', TRUE},	/* OP_FOLDDEL */
-    {'z', 'D', TRUE},	/* OP_FOLDDELREC */
-    {'g', 'w', TRUE},	/* OP_FORMAT2 */
-    {'g', '@', FALSE},	/* OP_FUNCTION */
-    {Ctrl_A, NUL, FALSE},	/* OP_NR_ADD */
-    {Ctrl_X, NUL, FALSE},	/* OP_NR_SUB */
+    {NUL, NUL, 0},			// OP_NOP
+    {'d', NUL, OPF_CHANGE},		// OP_DELETE
+    {'y', NUL, 0},			// OP_YANK
+    {'c', NUL, OPF_CHANGE},		// OP_CHANGE
+    {'<', NUL, OPF_LINES | OPF_CHANGE},	// OP_LSHIFT
+    {'>', NUL, OPF_LINES | OPF_CHANGE},	// OP_RSHIFT
+    {'!', NUL, OPF_LINES | OPF_CHANGE},	// OP_FILTER
+    {'g', '~', OPF_CHANGE},		// OP_TILDE
+    {'=', NUL, OPF_LINES | OPF_CHANGE},	// OP_INDENT
+    {'g', 'q', OPF_LINES | OPF_CHANGE},	// OP_FORMAT
+    {':', NUL, OPF_LINES},		// OP_COLON
+    {'g', 'U', OPF_CHANGE},		// OP_UPPER
+    {'g', 'u', OPF_CHANGE},		// OP_LOWER
+    {'J', NUL, OPF_LINES | OPF_CHANGE},	// DO_JOIN
+    {'g', 'J', OPF_LINES | OPF_CHANGE},	// DO_JOIN_NS
+    {'g', '?', OPF_CHANGE},		// OP_ROT13
+    {'r', NUL, OPF_CHANGE},		// OP_REPLACE
+    {'I', NUL, OPF_CHANGE},		// OP_INSERT
+    {'A', NUL, OPF_CHANGE},		// OP_APPEND
+    {'z', 'f', OPF_LINES},		// OP_FOLD
+    {'z', 'o', OPF_LINES},		// OP_FOLDOPEN
+    {'z', 'O', OPF_LINES},		// OP_FOLDOPENREC
+    {'z', 'c', OPF_LINES},		// OP_FOLDCLOSE
+    {'z', 'C', OPF_LINES},		// OP_FOLDCLOSEREC
+    {'z', 'd', OPF_LINES},		// OP_FOLDDEL
+    {'z', 'D', OPF_LINES},		// OP_FOLDDELREC
+    {'g', 'w', OPF_LINES | OPF_CHANGE},	// OP_FORMAT2
+    {'g', '@', OPF_CHANGE},		// OP_FUNCTION
+    {Ctrl_A, NUL, OPF_CHANGE},		// OP_NR_ADD
+    {Ctrl_X, NUL, OPF_CHANGE},		// OP_NR_SUB
 };
 
 /*
@@ -201,7 +205,16 @@
     int
 op_on_lines(int op)
 {
-    return opchars[op][2];
+    return opchars[op][2] & OPF_LINES;
+}
+
+/*
+ * Return TRUE if operator "op" changes text.
+ */
+    int
+op_is_change(int op)
+{
+    return opchars[op][2] & OPF_CHANGE;
 }
 
 /*
diff --git a/src/option.c b/src/option.c
index 7fd1dda..16d05d8 100644
--- a/src/option.c
+++ b/src/option.c
@@ -3229,7 +3229,7 @@
 static char *(p_scbopt_values[]) = {"ver", "hor", "jump", NULL};
 static char *(p_debug_values[]) = {"msg", "throw", "beep", NULL};
 static char *(p_ead_values[]) = {"both", "ver", "hor", NULL};
-static char *(p_buftype_values[]) = {"nofile", "nowrite", "quickfix", "help", "terminal", "acwrite", NULL};
+static char *(p_buftype_values[]) = {"nofile", "nowrite", "quickfix", "help", "terminal", "acwrite", "prompt", NULL};
 static char *(p_bufhidden_values[]) = {"hide", "unload", "delete", "wipe", NULL};
 static char *(p_bs_values[]) = {"indent", "eol", "start", NULL};
 #ifdef FEAT_FOLDING
diff --git a/src/proto/buffer.pro b/src/proto/buffer.pro
index 59bb2c2..9e63fb6 100644
--- a/src/proto/buffer.pro
+++ b/src/proto/buffer.pro
@@ -59,6 +59,7 @@
 int bt_quickfix(buf_T *buf);
 int bt_terminal(buf_T *buf);
 int bt_help(buf_T *buf);
+int bt_prompt(buf_T *buf);
 int bt_nofile(buf_T *buf);
 int bt_dontwrite(buf_T *buf);
 int bt_dontwrite_msg(buf_T *buf);
diff --git a/src/proto/channel.pro b/src/proto/channel.pro
index 8d26158..e6c9508 100644
--- a/src/proto/channel.pro
+++ b/src/proto/channel.pro
@@ -71,4 +71,5 @@
 void job_info(job_T *job, dict_T *dict);
 void job_info_all(list_T *l);
 int job_stop(job_T *job, typval_T *argvars, char *type);
+void invoke_prompt_callback(void);
 /* vim: set ft=c : */
diff --git a/src/proto/edit.pro b/src/proto/edit.pro
index 1f9e5b7..9ba7164 100644
--- a/src/proto/edit.pro
+++ b/src/proto/edit.pro
@@ -1,6 +1,8 @@
 /* edit.c */
 int edit(int cmdchar, int startln, long count);
 void edit_putchar(int c, int highlight);
+char_u *prompt_text(void);
+int prompt_curpos_editable(void);
 void edit_unputchar(void);
 void display_dollar(colnr_T col);
 void change_indent(int type, int amount, int round, int replaced, int call_changed_bytes);
diff --git a/src/proto/ops.pro b/src/proto/ops.pro
index 13e063e..01df56f 100644
--- a/src/proto/ops.pro
+++ b/src/proto/ops.pro
@@ -1,6 +1,7 @@
 /* ops.c */
 int get_op_type(int char1, int char2);
 int op_on_lines(int op);
+int op_is_change(int op);
 int get_op_char(int optype);
 int get_extra_op_char(int optype);
 void op_shift(oparg_T *oap, int curs_top, int amount);
diff --git a/src/structs.h b/src/structs.h
index b70b00f..cbebd7f 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -2356,6 +2356,11 @@
 
     int		b_shortname;	/* this file has an 8.3 file name */
 
+#ifdef FEAT_JOB_CHANNEL
+    char_u	*b_prompt_text;	     // set by prompt_setprompt()
+    char_u	*b_prompt_callback;  // set by prompt_setcallback()
+    partial_T	*b_prompt_partial;   // set by prompt_setcallback()
+#endif
 #ifdef FEAT_MZSCHEME
     void	*b_mzscheme_ref; /* The MzScheme reference to this buffer */
 #endif
diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak
index e36089a..6a0126b 100644
--- a/src/testdir/Make_all.mak
+++ b/src/testdir/Make_all.mak
@@ -147,6 +147,7 @@
 	    test_perl.res \
 	    test_plus_arg_edit.res \
 	    test_preview.res \
+	    test_prompt_buffer.res \
 	    test_profile.res \
 	    test_python2.res \
 	    test_python3.res \
diff --git a/src/testdir/screendump.vim b/src/testdir/screendump.vim
index 3c424a0..80d51c3 100644
--- a/src/testdir/screendump.vim
+++ b/src/testdir/screendump.vim
@@ -5,19 +5,18 @@
   finish
 endif
 
-" Need to be able to run terminal Vim with 256 colors.  On MS-Windows the
-" console only has 16 colors and the GUI can't run in a terminal.
-if !has('terminal') || has('win32')
-  func CanRunVimInTerminal()
-    return 0
-  endfunc
+" For most tests we need to be able to run terminal Vim with 256 colors.  On
+" MS-Windows the console only has 16 colors and the GUI can't run in a
+" terminal.
+func CanRunVimInTerminal()
+  return has('terminal') && !has('win32')
+endfunc
+
+" Skip the rest if there is no terminal feature at all.
+if !has('terminal')
   finish
 endif
 
-func CanRunVimInTerminal()
-  return 1
-endfunc
-
 source shared.vim
 
 " Run Vim with "arguments" in a new terminal window.
@@ -54,6 +53,7 @@
   let cols = get(a:options, 'cols', 75)
 
   let cmd = GetVimCommandClean()
+
   " Add -v to have gvim run in the terminal (if possible)
   let cmd .= ' -v ' . a:arguments
   let buf = term_start(cmd, {'curwin': 1, 'term_rows': rows, 'term_cols': cols})
@@ -64,11 +64,12 @@
     let cols = term_getsize(buf)[1]
   endif
 
-  " Wait for "All" or "Top" of the ruler in the status line to be shown.  This
-  " can be quite slow (e.g. when using valgrind).
+  " Wait for "All" or "Top" of the ruler to be shown in the last line or in
+  " the status line of the last window. This can be quite slow (e.g. when
+  " using valgrind).
   " If it fails then show the terminal contents for debugging.
   try
-    call WaitFor({-> len(term_getline(buf, rows)) >= cols - 1})
+    call WaitFor({-> len(term_getline(buf, rows)) >= cols - 1 || len(term_getline(buf, rows - 1)) >= cols - 1})
   catch /timed out after/
     let lines = map(range(1, rows), {key, val -> term_getline(buf, val)})
     call assert_report('RunVimInTerminal() failed, screen contents: ' . join(lines, "<NL>"))
@@ -80,7 +81,7 @@
 " Stop a Vim running in terminal buffer "buf".
 func StopVimInTerminal(buf)
   call assert_equal("running", term_getstatus(a:buf))
-  call term_sendkeys(a:buf, "\<Esc>\<Esc>:qa!\<cr>")
+  call term_sendkeys(a:buf, "\<Esc>:qa!\<cr>")
   call WaitForAssert({-> assert_equal("finished", term_getstatus(a:buf))})
   only!
 endfunc
diff --git a/src/testdir/test_prompt_buffer.vim b/src/testdir/test_prompt_buffer.vim
new file mode 100644
index 0000000..a21acc7
--- /dev/null
+++ b/src/testdir/test_prompt_buffer.vim
@@ -0,0 +1,55 @@
+" Tests for setting 'buftype' to "prompt"
+
+if !has('channel')
+  finish
+endif
+
+source shared.vim
+source screendump.vim
+
+func Test_prompt_basic()
+  " We need to use a terminal window to be able to feed keys without leaving
+  " Insert mode.
+  if !has('terminal')
+    call assert_report('no terminal')
+    return
+  endif
+  call writefile([
+	\ 'func TextEntered(text)',
+	\ '  if a:text == "exit"',
+	\ '    stopinsert',
+	\ '    close',
+	\ '  else',
+	\ '    " Add the output above the current prompt.',
+	\ '    call append(line("$") - 1, "Command: \"" . a:text . "\"")',
+	\ '    " Reset &modified to allow the buffer to be closed.',
+	\ '    set nomodified',
+	\ '    call timer_start(20, {id -> TimerFunc(a:text)})',
+	\ '  endif',
+	\ 'endfunc',
+	\ '',
+	\ 'func TimerFunc(text)',
+	\ '  " Add the output above the current prompt.',
+	\ '  call append(line("$") - 1, "Result: \"" . a:text . "\"")',
+	\ 'endfunc',
+	\ '',
+	\ 'call setline(1, "other buffer")',
+	\ 'new',
+	\ 'set buftype=prompt',
+	\ 'call prompt_setcallback(bufnr(""), function("TextEntered"))',
+	\ 'startinsert',
+	\ ], 'Xpromptscript')
+  let buf = RunVimInTerminal('-S Xpromptscript', {})
+  call WaitForAssert({-> assert_equal('%', term_getline(buf, 1))})
+
+  call term_sendkeys(buf, "hello\<CR>")
+  call WaitForAssert({-> assert_equal('% hello', term_getline(buf, 1))})
+  call WaitForAssert({-> assert_equal('Command: "hello"', term_getline(buf, 2))})
+  call WaitForAssert({-> assert_equal('Result: "hello"', term_getline(buf, 3))})
+
+  call term_sendkeys(buf, "exit\<CR>")
+  call WaitForAssert({-> assert_equal('other buffer', term_getline(buf, 1))})
+
+  call StopVimInTerminal(buf)
+  call delete('Xpromptscript')
+endfunc
diff --git a/src/version.c b/src/version.c
index 72960f3..985e659 100644
--- a/src/version.c
+++ b/src/version.c
@@ -762,6 +762,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    27,
+/**/
     26,
 /**/
     25,