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.
This commit is contained in:
Shadman
2026-02-14 23:18:08 +06:00
committed by GitHub
parent fb6a2c964d
commit 8ab511bba5
7 changed files with 177 additions and 77 deletions
+2 -1
View File
@@ -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;
}
-64
View File
@@ -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);
}
+4 -5
View File
@@ -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;
-1
View File
@@ -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);
+110
View File
@@ -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;
}
+2 -1
View File
@@ -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()) {
+59 -5
View File
@@ -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)