commit f4e88cfe429f990a4830f0dca255f77dae4ff149
parent a39334767321bf56dc6d0baedb048066fc56e11c
Author: Justin M. Keyes <justinkz@gmail.com>
Date: Mon, 16 Feb 2026 09:37:36 -0500
Merge #37875 prompt-buffer fixes
Diffstat:
5 files changed, 189 insertions(+), 14 deletions(-)
diff --git a/src/nvim/edit.c b/src/nvim/edit.c
@@ -1609,14 +1609,16 @@ static void init_prompt(int cmdchar_todo)
// prompt is missing, insert it or append a line with it
if (*text == NUL) {
ml_replace(curbuf->b_prompt_start.mark.lnum, prompt, true);
+ inserted_bytes(curbuf->b_prompt_start.mark.lnum, 0, 0, prompt_len);
} else {
- ml_append(curbuf->b_ml.ml_line_count, prompt, 0, false);
+ const linenr_T lnum = curbuf->b_ml.ml_line_count;
+ 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_start.mark.col = prompt_len;
curwin->w_cursor.lnum = curbuf->b_ml.ml_line_count;
coladvance(curwin, MAXCOL);
- inserted_bytes(curbuf->b_ml.ml_line_count, 0, 0, (colnr_T)prompt_len);
}
// Insert always starts after the prompt, allow editing text after it.
diff --git a/src/nvim/eval/buffer.c b/src/nvim/eval/buffer.c
@@ -20,6 +20,7 @@
#include "nvim/eval/typval_defs.h"
#include "nvim/eval/window.h"
#include "nvim/ex_cmds.h"
+#include "nvim/extmark.h"
#include "nvim/globals.h"
#include "nvim/macros_defs.h"
#include "nvim/memline.h"
@@ -790,6 +791,7 @@ void f_prompt_setprompt(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
// 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);
+ extmark_splice_cols(buf, prompt_lno - 1, 0, old_line_len, new_prompt_len, kExtmarkUndo);
cursor_col = new_prompt_len;
} else {
// Replace prev-prompt + user-input with new-prompt + user-input
@@ -797,14 +799,15 @@ void f_prompt_setprompt(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
if (ml_replace_buf(buf, prompt_lno, new_line, false, false) != OK) {
xfree(new_line);
}
+ extmark_splice_cols(buf, prompt_lno - 1, 0, buf->b_prompt_start.mark.col, new_prompt_len,
+ kExtmarkUndo);
cursor_col += new_prompt_len - old_prompt_len;
}
if (curwin->w_buffer == buf && curwin->w_cursor.lnum == prompt_lno) {
- coladvance(curwin, cursor_col);
+ curwin->w_cursor.col = cursor_col;
}
- changed_lines_redraw_buf(buf, prompt_lno, prompt_lno + 1, 0);
- redraw_buf_later(buf, UPD_INVERTED);
+ changed_lines(buf, prompt_lno, 0, prompt_lno + 1, 0, true);
}
// Clear old prompt text and replace with the new one
diff --git a/test/functional/api/extmark_spec.lua b/test/functional/api/extmark_spec.lua
@@ -11,7 +11,9 @@ local feed = n.feed
local clear = n.clear
local command = n.command
local exec = n.exec
+local exec_lua = n.exec_lua
local api = n.api
+local fn = n.fn
local assert_alive = n.assert_alive
local function expect(contents)
@@ -1562,10 +1564,54 @@ describe('API/extmarks', function()
it('in prompt buffer', function()
feed('dd')
- local id = set_extmark(ns, marks[1], 0, 0, {})
+ set_extmark(ns, marks[1], 0, 0, {})
api.nvim_set_option_value('buftype', 'prompt', {})
feed('i<esc>')
- eq({ { id, 0, 2 } }, get_extmarks(ns, 0, -1))
+ eq({ { marks[1], 0, 2 } }, get_extmarks(ns, 0, -1))
+ fn.prompt_setprompt('', 'foo > ')
+ eq({ { marks[1], 0, 6 } }, get_extmarks(ns, 0, -1))
+ feed('ihello')
+ eq({ { marks[1], 0, 11 } }, get_extmarks(ns, 0, -1))
+
+ local function get_extmark_range(id)
+ local rv = get_extmark_by_id(ns, id, { details = true })
+ return rv[3].invalid and 'invalid' or { rv[1], rv[2], rv[3].end_row, rv[3].end_col }
+ end
+
+ set_extmark(ns, marks[2], 0, 0, { invalidate = true, end_col = 6 })
+ set_extmark(ns, marks[3], 0, 6, { invalidate = true, end_col = 11 })
+ set_extmark(ns, marks[4], 0, 0, { invalidate = true, end_col = 11 })
+ set_extmark(ns, marks[5], 0, 0, { invalidate = true, end_row = 1 })
+ fn.prompt_setprompt('', 'floob > ')
+ eq({ 0, 13 }, get_extmark_range(marks[1]))
+ eq('invalid', get_extmark_range(marks[2])) -- extmark spanning old prompt invalidated
+ eq({ 0, 8, 0, 13 }, get_extmark_range(marks[3]))
+ eq({ 0, 8, 0, 13 }, get_extmark_range(marks[4]))
+ eq({ 0, 8, 1, 0 }, get_extmark_range(marks[5]))
+
+ set_extmark(ns, marks[2], 0, 0, { invalidate = true, end_col = 8 })
+ set_extmark(ns, marks[3], 0, 8, { invalidate = true, end_col = 13 })
+ set_extmark(ns, marks[4], 0, 0, { invalidate = true, end_col = 13 })
+ set_extmark(ns, marks[5], 0, 0, { invalidate = true, end_row = 1 })
+ -- Do this in the same event.
+ exec_lua(function()
+ vim.fn.setpos("':", { 0, 1, 999, 0 })
+ vim.fn.prompt_setprompt('', 'discard > ')
+ end)
+ eq({ 0, 10 }, get_extmark_range(marks[1]))
+ eq('invalid', get_extmark_range(marks[2])) -- all spans on line invalidated
+ eq('invalid', get_extmark_range(marks[3]))
+ eq('invalid', get_extmark_range(marks[4]))
+ eq({ 0, 10, 1, 0 }, get_extmark_range(marks[5]))
+
+ feed('hello')
+ eq({ 0, 15 }, get_extmark_range(marks[1]))
+ eq({ 0, 15, 1, 0 }, get_extmark_range(marks[5]))
+ -- init_prompt uses correct range for inserted_bytes when fixing empty prompt.
+ fn.setline('.', { '', 'last line' })
+ eq({ 'discard > ', 'last line' }, api.nvim_buf_get_lines(0, 0, -1, true))
+ eq({ 0, 10 }, get_extmark_range(marks[1]))
+ eq({ 0, 10, 1, 0 }, get_extmark_range(marks[5]))
end)
it('can get details', function()
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
|
@@ -892,10 +892,42 @@ describe('prompt buffer', function()
{1:~ }|*7
|
]])
+ -- Correct col when prompt has multi-cell chars.
+ feed('i<Left><Left>')
+ screen:expect([[
+ new-prompt > user input |
+ <>< user inp^ut |
+ {1:~ }|*7
+ {5:-- INSERT --} |
+ ]])
+ set_prompt('\t > ')
+ screen:expect([[
+ new-prompt > user input |
+ > user inp^ut |
+ {1:~ }|*7
+ {5:-- INSERT --} |
+ ]])
+ -- Works with 'virtualedit': coladd remains sensible. Cursor is redrawn correctly.
+ -- Tab size visually changes due to multiples of 'tabstop'.
+ command('set virtualedit=all')
+ feed('<C-O>Sa<Tab>b<C-O>3h')
+ screen:expect([[
+ new-prompt > user input |
+ > a ^ b |
+ {1:~ }|*7
+ {5:-- INSERT --} |
+ ]])
+ set_prompt('😊 > ')
+ screen:expect([[
+ new-prompt > user input |
+ 😊 > a ^ b |
+ {1:~ }|*7
+ {5:-- INSERT --} |
+ ]])
-- No crash when setting shorter prompt than curbuf's in other buffer.
- feed('i<C-O>zt')
- command('new | setlocal buftype=prompt')
+ feed('<C-O>zt')
+ command('set virtualedit& | new | setlocal buftype=prompt')
set_prompt('looooooooooooooooooooooooooooooooooooooooooooong > ', '') -- curbuf
set_prompt('foo > ')
screen:expect([[
@@ -904,7 +936,7 @@ describe('prompt buffer', function()
^ |
{1:~ }|
{3:[Prompt] [+] }|
- foo > user input |
+ foo > a b |
{1:~ }|*3
{5:-- INSERT --} |
]])
diff --git a/test/functional/lua/buffer_updates_spec.lua b/test/functional/lua/buffer_updates_spec.lua
@@ -422,6 +422,56 @@ describe('lua: nvim_buf_attach on_lines', function()
feed('<C-v>I <ESC>')
eq({ api.nvim_get_current_buf(), 0, 1, 1 }, exec_lua('return _G.res'))
end)
+
+ it('prompt buffer', function()
+ local check_events = setup_eventcheck(false, nil, {})
+ api.nvim_set_option_value('buftype', 'prompt', {})
+ feed('i')
+ check_events {
+ { 'test1', 'lines', 1, 4, 0, 1, 1, 1 },
+ }
+ fn.prompt_setprompt('', 'foo > ')
+ check_events {
+ { 'test1', 'lines', 1, 5, 0, 1, 1, 3 },
+ }
+ feed('hello')
+ check_events {
+ { 'test1', 'lines', 1, 6, 0, 1, 1, 7 },
+ }
+ fn.prompt_setprompt('', 'super-foo > ')
+ check_events {
+ { 'test1', 'lines', 1, 7, 0, 1, 1, 12 },
+ }
+ eq({ 'super-foo > hello' }, api.nvim_buf_get_lines(0, 0, -1, true))
+ -- Do this in the same event.
+ exec_lua(function()
+ vim.fn.setpos("':", { 0, 1, 999, 0 })
+ vim.fn.prompt_setprompt('', 'discard > ')
+ end)
+ check_events {
+ { 'test1', 'lines', 1, 8, 0, 1, 1, 18 },
+ }
+ eq({ 'discard > ' }, api.nvim_buf_get_lines(0, 0, -1, true))
+ feed('hello<S-CR>there')
+ check_events {
+ { 'test1', 'lines', 1, 9, 0, 1, 1, 11 },
+ { 'test1', 'lines', 1, 10, 0, 1, 2, 16 },
+ { 'test1', 'lines', 1, 11, 1, 2, 2, 1 },
+ }
+ fn.prompt_setprompt('', 'foo > ')
+ check_events {
+ { 'test1', 'lines', 1, 12, 0, 1, 1, 16 },
+ }
+ eq({ 'foo > hello', 'there' }, api.nvim_buf_get_lines(0, 0, -1, true))
+
+ -- init_prompt uses appended_lines_mark when appending to fix prompt.
+ api.nvim_buf_set_lines(0, 0, -1, true, { 'hi' })
+ eq({ 'hi', 'foo > ' }, api.nvim_buf_get_lines(0, 0, -1, true))
+ check_events {
+ { 'test1', 'lines', 1, 13, 0, 2, 1, 18 },
+ { 'test1', 'lines', 1, 14, 1, 1, 2, 0 },
+ }
+ end)
end)
describe('lua: nvim_buf_attach on_bytes', function()
@@ -1576,6 +1626,48 @@ describe('lua: nvim_buf_attach on_bytes', function()
{ 'test1', 'bytes', 1, 6, 2, 0, 6, 0, 0, 0, 1, 0, 1 },
{ 'test1', 'bytes', 1, 7, 2, 0, 6, 0, 0, 0, 0, 2, 2 },
}
+ fn.prompt_setprompt('', 'foo > ')
+ check_events {
+ { 'test1', 'bytes', 1, 8, 2, 0, 6, 0, 2, 2, 0, 6, 6 },
+ }
+ feed('hello')
+ check_events {
+ { 'test1', 'bytes', 1, 9, 2, 6, 12, 0, 0, 0, 0, 5, 5 },
+ }
+ fn.prompt_setprompt('', 'uber-foo > ')
+ check_events {
+ { 'test1', 'bytes', 1, 10, 2, 0, 6, 0, 6, 6, 0, 11, 11 },
+ }
+ eq({ '% ', '% ', 'uber-foo > hello' }, api.nvim_buf_get_lines(0, 0, -1, true))
+ -- Do this in the same event.
+ exec_lua(function()
+ vim.fn.setpos("':", { 0, vim.fn.line('.'), 999, 0 })
+ vim.fn.prompt_setprompt('', 'discard > ')
+ end)
+ check_events {
+ { 'test1', 'bytes', 1, 11, 2, 0, 6, 0, 16, 16, 0, 10, 10 },
+ }
+ eq({ '% ', '% ', 'discard > ' }, api.nvim_buf_get_lines(0, 0, -1, true))
+ feed('sup<S-CR>dood')
+ check_events {
+ { 'test1', 'bytes', 1, 12, 2, 10, 16, 0, 0, 0, 0, 3, 3 },
+ { 'test1', 'bytes', 1, 13, 2, 13, 19, 0, 0, 0, 1, 0, 1 },
+ { 'test1', 'bytes', 1, 14, 3, 0, 20, 0, 0, 0, 0, 4, 4 },
+ }
+ eq({ '% ', '% ', 'discard > sup', 'dood' }, api.nvim_buf_get_lines(0, 0, -1, true))
+ fn.prompt_setprompt('', 'cool > ')
+ check_events {
+ { 'test1', 'bytes', 1, 15, 2, 0, 6, 0, 10, 10, 0, 7, 7 },
+ }
+ eq({ '% ', '% ', 'cool > sup', 'dood' }, api.nvim_buf_get_lines(0, 0, -1, true))
+
+ -- init_prompt uses appended_lines_mark when appending to fix prompt.
+ api.nvim_buf_set_lines(0, 0, -1, true, { 'hi' })
+ eq({ 'hi', 'cool > ' }, api.nvim_buf_get_lines(0, 0, -1, true))
+ check_events {
+ { 'test1', 'bytes', 1, 16, 0, 0, 0, 4, 0, 22, 1, 0, 3 },
+ { 'test1', 'bytes', 1, 17, 1, 0, 3, 0, 0, 0, 1, 0, 8 },
+ }
end)
local function test_lockmarks(mode)