From a940b77cb25ae12cc1269cf64bdede698a4c5aac Mon Sep 17 00:00:00 2001 From: Shadman Date: Fri, 27 Mar 2026 17:39:09 +0600 Subject: [PATCH] feat(prompt): prompt_appendbuf() appends to prompt buffer #37763 Problem: Currently, we recommend always inserting text above prompt-line in prompt-buffer. This can be done using the `:` mark. However, although we recommend it this way it can sometimes get confusing how to do it best. Solution: Provide an api to append text to prompt buffer. This is a common use-case for things using prompt-buffer. --- runtime/doc/channel.txt | 4 +- runtime/doc/news.txt | 1 + runtime/doc/vim_diff.txt | 1 + runtime/doc/vimfn.txt | 25 ++++++ runtime/lua/vim/_meta/vimfn.lua | 22 ++++++ src/nvim/buffer.c | 1 + src/nvim/buffer_defs.h | 1 + src/nvim/edit.c | 9 ++- src/nvim/eval.c | 1 + src/nvim/eval.lua | 26 +++++++ src/nvim/eval/buffer.c | 76 ++++++++++++++++++- test/functional/legacy/prompt_buffer_spec.lua | 8 +- 12 files changed, 167 insertions(+), 8 deletions(-) diff --git a/runtime/doc/channel.txt b/runtime/doc/channel.txt index fe34363aca..67aba15984 100644 --- a/runtime/doc/channel.txt +++ b/runtime/doc/channel.txt @@ -235,8 +235,8 @@ command, displaying shell output above the prompt: >lua -- Handles output from the shell. local function on_output(_, msg, _) -- Add shell output above the prompt. - local input_start = vim.api.nvim_buf_get_mark(0, ":")[1] - vim.fn.append(input_start - 1, msg) + local buf = vim.api.nvim_get_current_buf() + vim.fn.prompt_appendbuf(buf, msg) end -- Handles the shell exit. diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 8461bafab2..cd39186e4a 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -488,6 +488,7 @@ VIMSCRIPT • |prompt_getinput()| gets current user-input in prompt-buffer. • |wildtrigger()| triggers command-line expansion. • |v:vim_did_init| is set after sourcing |init.vim| but before |load-plugins|. +• |prompt_appendbuf()| appends text to prompt-buffer. ============================================================================== CHANGED FEATURES *news-changed* diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 177f765739..6652d6b595 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -355,6 +355,7 @@ Functions: - |tempname()| tries to recover if the Nvim |tempdir| disappears. - |writefile()| with "p" flag creates parent directories. - |prompt_getinput()| +- |prompt_appendbuf()| Highlight groups: - |highlight-blend| controls blend level for a highlight group diff --git a/runtime/doc/vimfn.txt b/runtime/doc/vimfn.txt index 51f003ec3d..103e44fbc7 100644 --- a/runtime/doc/vimfn.txt +++ b/runtime/doc/vimfn.txt @@ -7575,6 +7575,31 @@ printf({fmt}, {expr1} ...) *printf()* Return: ~ (`string`) +prompt_appendbuf({buf}, {text}) *prompt_appendbuf()* + Appends text to prompt buffer before current prompt. When {text} is + a |List|: Append each item of the |List| as a text line above + prompt-line in the buffer. Any type of item is accepted and converted + to a String. Returns 1 for failure ({buf} not a prmopt buffer), + 0 for success. When {text} is an empty list zero is returned. + + Example: >vim + func TextEntered(text) + call prompt_appendbuf(bufnr(''), split('Entered: "' . a:text . '"', '\n')) + endfunc + + set buftype=prompt + call prompt_setcallback(bufnr(''), function("TextEntered")) + eval bufnr("")->prompt_setprompt("cmd: ") + startinsert +< + + Parameters: ~ + • {buf} (`integer|string`) + • {text} (`string|string[]`) + + Return: ~ + (`0|1`) + prompt_getinput({buf}) *prompt_getinput()* Gets the current user-input in |prompt-buffer| {buf} without invoking prompt_callback. {buf} can be a buffer name or number. diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua index 1d3412d20a..a5822c9c84 100644 --- a/runtime/lua/vim/_meta/vimfn.lua +++ b/runtime/lua/vim/_meta/vimfn.lua @@ -6893,6 +6893,28 @@ function vim.fn.prevnonblank(lnum) end --- @return string function vim.fn.printf(fmt, expr1) end +--- Appends text to prompt buffer before current prompt. When {text} is +--- a |List|: Append each item of the |List| as a text line above +--- prompt-line in the buffer. Any type of item is accepted and converted +--- to a String. Returns 1 for failure ({buf} not a prmopt buffer), +--- 0 for success. When {text} is an empty list zero is returned. +--- +--- Example: >vim +--- func TextEntered(text) +--- call prompt_appendbuf(bufnr(''), split('Entered: "' . a:text . '"', '\n')) +--- endfunc +--- +--- set buftype=prompt +--- call prompt_setcallback(bufnr(''), function("TextEntered")) +--- eval bufnr("")->prompt_setprompt("cmd: ") +--- startinsert +--- < +--- +--- @param buf integer|string +--- @param text string|string[] +--- @return 0|1 +function vim.fn.prompt_appendbuf(buf, text) end + --- Gets the current user-input in |prompt-buffer| {buf} without invoking --- prompt_callback. {buf} can be a buffer name or number. --- diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 8f83686f4e..02f93bc64a 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -2107,6 +2107,7 @@ buf_T *buflist_new(char *ffname_arg, char *sfname_arg, linenr_T lnum, int flags) buf->b_prompt_text = NULL; buf->b_prompt_start = (fmark_T)INIT_FMARK; buf->b_prompt_start.mark.col = 2; // default prompt is "% " + buf->b_prompt_append_new_line = true; return buf; } diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 202d9fefff..24d2b677f1 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -715,6 +715,7 @@ struct file_buffer { char *b_prompt_text; // set by prompt_setprompt() Callback b_prompt_callback; // set by prompt_setcallback() Callback b_prompt_interrupt; // set by prompt_setinterrupt() + bool b_prompt_append_new_line; // prompt_appendlines() should start a newline int b_prompt_insert; // value for restart_edit when entering // a prompt buffer window. fmark_T b_prompt_start; // Start of the editable area of a prompt buffer. diff --git a/src/nvim/edit.c b/src/nvim/edit.c index bc166e39e8..5744f86cf6 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -1594,8 +1594,12 @@ static void init_prompt(int cmdchar_todo) int prompt_len = (int)strlen(prompt); // In case the mark is set to a nonexistent line. - curbuf->b_prompt_start.mark.lnum = MAX(1, MIN(curbuf->b_prompt_start.mark.lnum, - curbuf->b_ml.ml_line_count)); + if (curbuf->b_prompt_start.mark.lnum < 1 + || curbuf->b_prompt_start.mark.lnum > curbuf->b_ml.ml_line_count) { + curbuf->b_prompt_start.mark.lnum = MAX(1, MIN(curbuf->b_prompt_start.mark.lnum, + curbuf->b_ml.ml_line_count)); + curbuf->b_prompt_append_new_line = true; + } curwin->w_cursor.lnum = MAX(curwin->w_cursor.lnum, curbuf->b_prompt_start.mark.lnum); char *text = ml_get(curbuf->b_prompt_start.mark.lnum); @@ -1615,6 +1619,7 @@ static void init_prompt(int cmdchar_todo) ml_append(lnum, prompt, 0, false); appended_lines_mark(lnum, 1); curbuf->b_prompt_start.mark.lnum = curbuf->b_ml.ml_line_count; + curbuf->b_prompt_append_new_line = true; // Like submitting, undo history was relevant to the old prompt. u_clearallandblockfree(curbuf); } diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 49302fdc53..981a99ec48 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -6725,6 +6725,7 @@ theend: u_clearallandblockfree(curbuf); curbuf->b_prompt_start.mark.lnum = curbuf->b_ml.ml_line_count; + curbuf->b_prompt_append_new_line = true; } /// @return true when the interrupt callback was invoked. diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index f8539b907d..a75c8720dd 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -8371,6 +8371,32 @@ M.funcs = { signature = 'printf({fmt}, {expr1} ...)', returns = 'string', }, + prompt_appendbuf = { + args = 2, + base = 2, + desc = [=[ + Appends text to prompt buffer before current prompt. When {text} is + a |List|: Append each item of the |List| as a text line above + prompt-line in the buffer. Any type of item is accepted and converted + to a String. Returns 1 for failure ({buf} not a prmopt buffer), + 0 for success. When {text} is an empty list zero is returned. + + Example: >vim + func TextEntered(text) + call prompt_appendbuf(bufnr(''), split('Entered: "' . a:text . '"', '\n')) + endfunc + + set buftype=prompt + call prompt_setcallback(bufnr(''), function("TextEntered")) + eval bufnr("")->prompt_setprompt("cmd: ") + startinsert + < + ]=], + name = 'prompt_appendbuf', + params = { { 'buf', 'integer|string' }, { 'text', 'string|string[]' } }, + returns = '0|1', + signature = 'prompt_appendbuf({buf}, {text})', + }, prompt_getinput = { args = 1, base = 1, diff --git a/src/nvim/eval/buffer.c b/src/nvim/eval/buffer.c index 06748c0c64..5309f8257e 100644 --- a/src/nvim/eval/buffer.c +++ b/src/nvim/eval/buffer.c @@ -274,6 +274,74 @@ void f_appendbufline(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) buf_set_append_line(argvars, rettv, true); } +/// "prompt_appendbuf({buffer}, string/list)" function +void f_prompt_appendbuf(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) + FUNC_ATTR_NONNULL_ALL +{ + const int did_emsg_before = did_emsg; + + // Return an 1 by default, e.g. append failed or not a prompt buffer + rettv->v_type = VAR_NUMBER; + rettv->vval.v_number = 1; + + buf_T *const buf = tv_get_buf_from_arg(&argvars[0]); + if (buf == NULL || !bt_prompt(buf)) { + return; + } + + linenr_T lnum = MAX(0, buf->b_prompt_start.mark.lnum - 1); + typval_T *lines = &argvars[1]; + if (!buf->b_prompt_append_new_line) { + // Since we are not creating a new line we need to append input to current line + const char *text = (lnum > 0) ? (const char *)ml_get_buf(buf, lnum) : ""; + if (lines->v_type == VAR_LIST) { + list_T *l = lines->vval.v_list; + if (l != NULL && tv_list_len(l) > 0) { + listitem_T *li = tv_list_first(l); + const char *str = tv_get_string(&li->li_tv); + char *new_str = concat_str(text, str); + tv_clear(&li->li_tv); + li->li_tv.v_type = VAR_STRING; + li->li_tv.vval.v_string = new_str; + } + } else if (lines->v_type == VAR_STRING) { + const char *str = tv_get_string(lines); + char *new_str = concat_str(text, str); + tv_clear(lines); + lines->v_type = VAR_STRING; + lines->vval.v_string = new_str; + } + } + + if (did_emsg == did_emsg_before) { + set_buffer_lines(buf, lnum, buf->b_prompt_append_new_line, lines, rettv); + } + + if (rettv->vval.v_number == 0) { + // Ok we've inserted the lines successfully now check if last string ended with '\n' + // to determine if we need to insert a new line before next append + buf->b_prompt_append_new_line = false; + if (lines->v_type == VAR_LIST) { + list_T *l = lines->vval.v_list; + if (l != NULL && tv_list_len(l) > 0) { + listitem_T *li = tv_list_last(l); + const char *str = tv_get_string(&li->li_tv); + size_t len = strlen(str); + if (len > 0 && str[len - 1] == '\n') { + buf->b_prompt_append_new_line = true; + } + } + } else if (lines->v_type == VAR_STRING) { + const char *str = tv_get_string(lines); + size_t len = strlen(str); + + if (len > 0 && str[len - 1] == '\n') { + buf->b_prompt_append_new_line = true; + } + } + } +} + /// "bufadd(expr)" function void f_bufadd(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { @@ -778,8 +846,12 @@ void f_prompt_setprompt(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) // even while user is editing their input. if (bt_prompt(buf) && buf->b_ml.ml_mfp != NULL) { // In case the mark is set to a nonexistent line. - buf->b_prompt_start.mark.lnum = MAX(1, MIN(buf->b_prompt_start.mark.lnum, - buf->b_ml.ml_line_count)); + if (buf->b_prompt_start.mark.lnum < 1 + || buf->b_prompt_start.mark.lnum > curbuf->b_ml.ml_line_count) { + buf->b_prompt_start.mark.lnum = MAX(1, MIN(buf->b_prompt_start.mark.lnum, + buf->b_ml.ml_line_count)); + curbuf->b_prompt_append_new_line = true; + } linenr_T prompt_lno = buf->b_prompt_start.mark.lnum; char *old_prompt = buf_prompt_text(buf); diff --git a/test/functional/legacy/prompt_buffer_spec.lua b/test/functional/legacy/prompt_buffer_spec.lua index 81893d9067..2ba7c9db9f 100644 --- a/test/functional/legacy/prompt_buffer_spec.lua +++ b/test/functional/legacy/prompt_buffer_spec.lua @@ -31,14 +31,18 @@ describe('prompt buffer', function() close else " Add the output above the current prompt. - call append(line("$") - 1, split('Command: "' . a:text . '"', '\n')) + call prompt_appendbuf(bufnr(''), split('Command: "' . a:text . '"', '\n')) + " 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, split('Result: "' . a:text .'"', '\n')) + call prompt_appendbuf(bufnr(''), split('Result: "' . a:text .'"', '\n')) + " Reset &modified to allow the buffer to be closed. + set nomodified endfunc func SwitchWindows()