neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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:
Msrc/nvim/buffer.c | 3++-
Msrc/nvim/channel.c | 64----------------------------------------------------------------
Msrc/nvim/edit.c | 9++++-----
Msrc/nvim/eval.c | 1-
Msrc/nvim/eval/buffer.c | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/nvim/optionstr.c | 3++-
Mtest/functional/legacy/prompt_buffer_spec.lua | 64+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
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)