commit 8ab511bba524bcd5b5913d1b1205b5e4fe3f7210
parent fb6a2c964d258a72f4ae9683254dafe3cc3ea26c
Author: Shadman <shadmansaleh3@gmail.com>
Date: Sat, 14 Feb 2026 23:18:08 +0600
feat(prompt): plugins can update prompt during user input #37743
Problem:
Currently, if prompt gets changed during user-input with
prompt_setprompt() it only gets reflected in next prompt. And that
behavior is not also consistent. If user re-enters insert mode then the
user input gets discarded and a new prompt gets created with the new
prompt.
Solution:
Handle prompt_setprompt eagerly. Update the prompt display, preserve user input.
Diffstat:
7 files changed, 177 insertions(+), 77 deletions(-)
diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c
@@ -2105,7 +2105,8 @@ buf_T *buflist_new(char *ffname_arg, char *sfname_arg, linenr_T lnum, int flags)
buf->b_prompt_callback.type = kCallbackNone;
buf->b_prompt_interrupt.type = kCallbackNone;
buf->b_prompt_text = NULL;
- clear_fmark(&buf->b_prompt_start, 0);
+ buf->b_prompt_start = (fmark_T)INIT_FMARK;
+ buf->b_prompt_start.mark.col = 2; // default prompt is "% "
return buf;
}
diff --git a/src/nvim/channel.c b/src/nvim/channel.c
@@ -17,7 +17,6 @@
#include "nvim/errors.h"
#include "nvim/eval.h"
#include "nvim/eval/encode.h"
-#include "nvim/eval/funcs.h"
#include "nvim/eval/typval.h"
#include "nvim/event/loop.h"
#include "nvim/event/multiqueue.h"
@@ -26,7 +25,6 @@
#include "nvim/event/socket.h"
#include "nvim/event/stream.h"
#include "nvim/event/wstream.h"
-#include "nvim/ex_cmds.h"
#include "nvim/garray.h"
#include "nvim/gettext_defs.h"
#include "nvim/globals.h"
@@ -1009,65 +1007,3 @@ Array channel_all_info(Arena *arena)
}
return ret;
}
-
-/// "prompt_setcallback({buffer}, {callback})" function
-void f_prompt_setcallback(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
-{
- Callback prompt_callback = { .type = kCallbackNone };
-
- if (check_secure()) {
- return;
- }
- buf_T *buf = tv_get_buf(&argvars[0], false);
- if (buf == NULL) {
- return;
- }
-
- if (argvars[1].v_type != VAR_STRING || *argvars[1].vval.v_string != NUL) {
- if (!callback_from_typval(&prompt_callback, &argvars[1])) {
- return;
- }
- }
-
- callback_free(&buf->b_prompt_callback);
- buf->b_prompt_callback = prompt_callback;
-}
-
-/// "prompt_setinterrupt({buffer}, {callback})" function
-void f_prompt_setinterrupt(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
-{
- Callback interrupt_callback = { .type = kCallbackNone };
-
- if (check_secure()) {
- return;
- }
- buf_T *buf = tv_get_buf(&argvars[0], false);
- if (buf == NULL) {
- return;
- }
-
- if (argvars[1].v_type != VAR_STRING || *argvars[1].vval.v_string != NUL) {
- if (!callback_from_typval(&interrupt_callback, &argvars[1])) {
- return;
- }
- }
-
- callback_free(&buf->b_prompt_interrupt);
- buf->b_prompt_interrupt = interrupt_callback;
-}
-
-/// "prompt_setprompt({buffer}, {text})" function
-void f_prompt_setprompt(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
-{
- if (check_secure()) {
- return;
- }
- buf_T *buf = tv_get_buf(&argvars[0], false);
- if (buf == NULL) {
- return;
- }
-
- const char *text = tv_get_string(&argvars[1]);
- xfree(buf->b_prompt_text);
- buf->b_prompt_text = xstrdup(text);
-}
diff --git a/src/nvim/edit.c b/src/nvim/edit.c
@@ -1596,15 +1596,14 @@ static void init_prompt(int cmdchar_todo)
if (curwin->w_cursor.lnum < curbuf->b_prompt_start.mark.lnum) {
curwin->w_cursor.lnum = curbuf->b_prompt_start.mark.lnum;
}
- char *text = get_cursor_line_ptr();
+ char *text = ml_get(curbuf->b_prompt_start.mark.lnum);
if ((curbuf->b_prompt_start.mark.lnum == curwin->w_cursor.lnum
&& (curbuf->b_prompt_start.mark.col < prompt_len
- || strncmp(text + curbuf->b_prompt_start.mark.col - prompt_len, prompt,
- (size_t)prompt_len) != 0))
- || curbuf->b_prompt_start.mark.lnum > curwin->w_cursor.lnum) {
+ || !strnequal(text + curbuf->b_prompt_start.mark.col - prompt_len, prompt,
+ (size_t)prompt_len)))) {
// prompt is missing, insert it or append a line with it
if (*text == NUL) {
- ml_replace(curbuf->b_ml.ml_line_count, prompt, true);
+ ml_replace(curbuf->b_prompt_start.mark.lnum, prompt, true);
} else {
ml_append(curbuf->b_ml.ml_line_count, prompt, 0, false);
curbuf->b_prompt_start.mark.lnum = curbuf->b_ml.ml_line_count;
diff --git a/src/nvim/eval.c b/src/nvim/eval.c
@@ -6669,7 +6669,6 @@ void prompt_invoke_callback(void)
curwin->w_cursor.lnum = lnum + 1;
curwin->w_cursor.col = 0;
curbuf->b_prompt_start.mark.lnum = lnum + 1;
- curbuf->b_prompt_start.mark.col = 0;
if (curbuf->b_prompt_callback.type == kCallbackNone) {
xfree(user_input);
diff --git a/src/nvim/eval/buffer.c b/src/nvim/eval/buffer.c
@@ -11,12 +11,15 @@
#include "nvim/buffer_defs.h"
#include "nvim/change.h"
#include "nvim/cursor.h"
+#include "nvim/drawscreen.h"
+#include "nvim/edit.h"
#include "nvim/eval.h"
#include "nvim/eval/buffer.h"
#include "nvim/eval/funcs.h"
#include "nvim/eval/typval.h"
#include "nvim/eval/typval_defs.h"
#include "nvim/eval/window.h"
+#include "nvim/ex_cmds.h"
#include "nvim/globals.h"
#include "nvim/macros_defs.h"
#include "nvim/memline.h"
@@ -25,6 +28,7 @@
#include "nvim/path.h"
#include "nvim/pos_defs.h"
#include "nvim/sign.h"
+#include "nvim/strings.h"
#include "nvim/types_defs.h"
#include "nvim/undo.h"
#include "nvim/vim_defs.h"
@@ -704,3 +708,109 @@ void restore_buffer(bufref_T *save_curbuf)
curbuf->b_nwindows++;
}
}
+
+/// "prompt_setcallback({buffer}, {callback})" function
+void f_prompt_setcallback(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
+{
+ Callback prompt_callback = { .type = kCallbackNone };
+
+ if (check_secure()) {
+ return;
+ }
+ buf_T *buf = tv_get_buf(&argvars[0], false);
+ if (buf == NULL) {
+ return;
+ }
+
+ if (argvars[1].v_type != VAR_STRING || *argvars[1].vval.v_string != NUL) {
+ if (!callback_from_typval(&prompt_callback, &argvars[1])) {
+ return;
+ }
+ }
+
+ callback_free(&buf->b_prompt_callback);
+ buf->b_prompt_callback = prompt_callback;
+}
+
+/// "prompt_setinterrupt({buffer}, {callback})" function
+void f_prompt_setinterrupt(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
+{
+ Callback interrupt_callback = { .type = kCallbackNone };
+
+ if (check_secure()) {
+ return;
+ }
+ buf_T *buf = tv_get_buf(&argvars[0], false);
+ if (buf == NULL) {
+ return;
+ }
+
+ if (argvars[1].v_type != VAR_STRING || *argvars[1].vval.v_string != NUL) {
+ if (!callback_from_typval(&interrupt_callback, &argvars[1])) {
+ return;
+ }
+ }
+
+ callback_free(&buf->b_prompt_interrupt);
+ buf->b_prompt_interrupt = interrupt_callback;
+}
+
+/// "prompt_setprompt({buffer}, {text})" function
+void f_prompt_setprompt(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
+{
+ if (check_secure()) {
+ return;
+ }
+ buf_T *buf = tv_get_buf(&argvars[0], false);
+ if (buf == NULL) {
+ return;
+ }
+
+ const char *new_prompt = tv_get_string(&argvars[1]);
+ int new_prompt_len = (int)strlen(new_prompt);
+
+ // Update the prompt-text and prompt-marks if a plugin calls prompt_setprompt()
+ // even while user is editing their input.
+ if (bt_prompt(buf)) {
+ if (buf->b_prompt_start.mark.lnum > buf->b_ml.ml_line_count) {
+ // In case the mark is set to a nonexistent line.
+ buf->b_prompt_start.mark.lnum = buf->b_ml.ml_line_count;
+ }
+
+ linenr_T prompt_lno = buf->b_prompt_start.mark.lnum;
+ char *old_prompt = buf_prompt_text(buf);
+ char *old_line = ml_get_buf(buf, prompt_lno);
+ old_line = old_line != NULL ? old_line : "";
+
+ int old_prompt_len = (int)strlen(old_prompt);
+ colnr_T cursor_col = curwin->w_cursor.col;
+
+ if (buf->b_prompt_start.mark.col < old_prompt_len
+ || curbuf->b_prompt_start.mark.col < old_prompt_len
+ || !strnequal(old_prompt, old_line + curbuf->b_prompt_start.mark.col - old_prompt_len,
+ (size_t)old_prompt_len)) {
+ // If for some odd reason the old prompt is missing,
+ // replace prompt line with new-prompt (discards user-input).
+ ml_replace_buf(buf, prompt_lno, (char *)new_prompt, true, false);
+ cursor_col = new_prompt_len;
+ } else {
+ // Replace prev-prompt + user-input with new-prompt + user-input
+ char *new_line = concat_str(new_prompt, old_line + buf->b_prompt_start.mark.col);
+ if (ml_replace_buf(buf, prompt_lno, new_line, false, false) != OK) {
+ xfree(new_line);
+ }
+ cursor_col += new_prompt_len - old_prompt_len;
+ }
+
+ if (curwin->w_buffer == buf) {
+ coladvance(curwin, cursor_col);
+ }
+ changed_lines_redraw_buf(buf, prompt_lno, prompt_lno + 1, 0);
+ redraw_buf_later(buf, UPD_INVERTED);
+ }
+
+ // Clear old prompt text and replace with the new one
+ xfree(buf->b_prompt_text);
+ buf->b_prompt_text = xstrdup(new_prompt);
+ buf->b_prompt_start.mark.col = (colnr_T)new_prompt_len;
+}
diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c
@@ -711,7 +711,8 @@ const char *did_set_buftype(optset_T *args)
// Set default value for 'comments'
set_option_direct(kOptComments, STATIC_CSTR_AS_OPTVAL(""), OPT_LOCAL, SID_NONE);
// set the prompt start position to lastline.
- pos_T next_prompt = { .lnum = buf->b_ml.ml_line_count, .col = 0, .coladd = 0 };
+ pos_T next_prompt = { .lnum = buf->b_ml.ml_line_count, .col = buf->b_prompt_start.mark.col,
+ .coladd = 0 };
RESET_FMARK(&buf->b_prompt_start, next_prompt, 0, ((fmarkv_T)INIT_FMARKV));
}
if (win->w_status_height || global_stl_height()) {
diff --git a/test/functional/legacy/prompt_buffer_spec.lua b/test/functional/legacy/prompt_buffer_spec.lua
@@ -62,7 +62,7 @@ describe('prompt buffer', function()
screen:expect([[
cmd: ^ |
{1:~ }|*3
- {3:[Prompt] [+] }|
+ {3:[Prompt] }|
other buffer |
{1:~ }|*3
{5:-- INSERT --} |
@@ -149,7 +149,7 @@ describe('prompt buffer', function()
screen:expect([[
cmd: |
{1:~ }|*3
- {2:[Prompt] [+] }|
+ {2:[Prompt] }|
^other buffer |
{1:~ }|*3
|
@@ -158,7 +158,7 @@ describe('prompt buffer', function()
screen:expect([[
cmd: ^ |
{1:~ }|*3
- {3:[Prompt] [+] }|
+ {3:[Prompt] }|
other buffer |
{1:~ }|*3
{5:-- INSERT --} |
@@ -167,7 +167,7 @@ describe('prompt buffer', function()
screen:expect([[
cmd:^ |
{1:~ }|*3
- {3:[Prompt] [+] }|
+ {3:[Prompt] }|
other buffer |
{1:~ }|*3
|
@@ -290,8 +290,8 @@ describe('prompt buffer', function()
source([[
bwipeout!
set formatoptions+=r
- set buftype=prompt
call prompt_setprompt(bufnr(), "% ")
+ set buftype=prompt
]])
feed('iline1<s-cr>line2')
screen:expect([[
@@ -793,4 +793,58 @@ describe('prompt buffer', function()
eq({ 2, 0 }, api.nvim_buf_get_mark(0, ':'))
end)
+
+ it('prompt can be changed without interrupting user input', function()
+ api.nvim_set_option_value('buftype', 'prompt', { buf = 0 })
+ local buf = api.nvim_get_current_buf()
+
+ local function set_prompt(prompt)
+ fn('prompt_setprompt', buf, prompt)
+ end
+
+ set_prompt('> ')
+
+ source('startinsert')
+
+ feed('user input')
+ -- Move the cursor a bit to check cursor maintaining position
+ feed('<esc>hhi')
+
+ screen:expect([[
+ > user in^put |
+ {1:~ }|*8
+ {5:-- INSERT --} |
+ ]])
+
+ eq({ 1, 2 }, api.nvim_buf_get_mark(0, ':'))
+
+ set_prompt('new-prompt > ')
+
+ screen:expect([[
+ new-prompt > user in^put |
+ {1:~ }|*8
+ {5:-- INSERT --} |
+ ]])
+
+ eq({ 1, 13 }, api.nvim_buf_get_mark(0, ':'))
+
+ set_prompt('new-prompt(status) > ')
+
+ screen:expect([[
+ new-prompt(status) > user|
+ in^put |
+ {1:~ }|*7
+ {5:-- INSERT --} |
+ ]])
+ eq({ 1, 21 }, api.nvim_buf_get_mark(0, ':'))
+
+ set_prompt('new-prompt > ')
+
+ screen:expect([[
+ new-prompt > user in^put |
+ {1:~ }|*8
+ {5:-- INSERT --} |
+ ]])
+ eq({ 1, 13 }, api.nvim_buf_get_mark(0, ':'))
+ end)
end)