commit 3a10405214ace2ac4bf7a289bd789fa16e18c437
parent 7641177c5f981d90b3c080763a401496034f35c9
Author: Sean Dewar <6256228+seandewar@users.noreply.github.com>
Date: Sun, 15 Feb 2026 16:22:47 +0000
fix(prompt): prompt_setprompt does not adjust extmarks, no on_bytes
Problem: prompt_setprompt does not adjust extmarks or trigger on_bytes
buffer-updates when fixing the prompt line.
Solution: adjust them, trigger on_bytes.
Notably, hides extmarks when replacing the entire line (and clearing user
input). Otherwise, when just replacing the prompt text, hides extmarks there,
but moves those after (in the user input area) to the correct spot.
Diffstat:
3 files changed, 77 insertions(+), 2 deletions(-)
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,6 +799,8 @@ 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;
}
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,45 @@ 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]))
end)
it('can get details', function()
diff --git a/test/functional/lua/buffer_updates_spec.lua b/test/functional/lua/buffer_updates_spec.lua
@@ -1618,6 +1618,40 @@ 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))
end)
local function test_lockmarks(mode)