From 602cbbe1d93095c204b7858d535c459e09298953 Mon Sep 17 00:00:00 2001 From: Sean Dewar <6256228+seandewar@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:48:19 +0000 Subject: [PATCH 1/2] fix(prompt): prompt_setprompt cursor col adjustment Problem: prompt_setprompt adjusted cursor col may be negative (<=0 when 1-based), and doesn't check the col of ': Solution: avoid negative col and adjust correctly if ': col differs from old prompt's length. --- src/nvim/eval/buffer.c | 3 +- test/functional/legacy/prompt_buffer_spec.lua | 29 +++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/nvim/eval/buffer.c b/src/nvim/eval/buffer.c index c550b6b743..f5b43fc4db 100644 --- a/src/nvim/eval/buffer.c +++ b/src/nvim/eval/buffer.c @@ -802,11 +802,12 @@ void f_prompt_setprompt(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) } 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; + cursor_col += new_prompt_len - buf->b_prompt_start.mark.col; } if (curwin->w_buffer == buf && curwin->w_cursor.lnum == prompt_lno) { curwin->w_cursor.col = cursor_col; + check_cursor_col(curwin); } changed_lines(buf, prompt_lno, 0, prompt_lno + 1, 0, true); } diff --git a/test/functional/legacy/prompt_buffer_spec.lua b/test/functional/legacy/prompt_buffer_spec.lua index 0e6b665310..9ac6ae6ac9 100644 --- a/test/functional/legacy/prompt_buffer_spec.lua +++ b/test/functional/legacy/prompt_buffer_spec.lua @@ -931,9 +931,34 @@ describe('prompt buffer', function() {1:~ }|*7 {5:-- INSERT --} | ]]) + -- Minimum col should be 1. Same event to avoid corrections from the state loop. + feed('0') + local colnr = exec_lua(function() + vim.fn.prompt_setprompt('', '') + return vim.fn.col('.') + end) + eq(1, colnr) + -- Correct cursor adjustment when old ': col and old prompt length differs. + set_prompt('foo > ') + fn('setpos', "':", { 0, fn('line', '.'), 10, 0 }) + fn('setline', '.', ' foo > hello') + feed('fh') + screen:expect([[ + new-prompt > user input | + foo > ^hello | + {1:~ }|*7 + | + ]]) + set_prompt('bar > ') + screen:expect([[ + new-prompt > user input | + bar > ^hello | + {1:~ }|*7 + | + ]]) -- No crash when setting shorter prompt than curbuf's in other buffer. - feed('zt') + feed('ztA') command('set virtualedit& | new | setlocal buftype=prompt') set_prompt('looooooooooooooooooooooooooooooooooooooooooooong > ', '') -- curbuf set_prompt('foo > ') @@ -943,7 +968,7 @@ describe('prompt buffer', function() ^ | {1:~ }| {3:[Prompt] [+] }| - foo > a b | + foo > hello | {1:~ }|*3 {5:-- INSERT --} | ]]) From a5eb023a53932be640c31a969fa1923e1002e2ad Mon Sep 17 00:00:00 2001 From: Sean Dewar <6256228+seandewar@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:34:42 +0000 Subject: [PATCH 2/2] fix(prompt): clear undo when changing/appending prompt Problem: undoing after the prompt is changed breaks it (and causes init_prompt to abort it and append a new one), as the undo history contains the old prompt. Solution: like submitting, clear the undo buffer. Don't do it in init_prompt if the line was empty; that may not result in a new prompt, and causes commands like "S" to lose the history. As u_save, etc. wasn't being called by prompt_setprompt, undoing after it fixes the prompt usually gave undesirable results anyway. Remove the added undo_spec.lua test, as its approach no longer works as a repro, and finding a new one seems fiddly. --- src/nvim/edit.c | 2 + src/nvim/eval/buffer.c | 6 ++- test/functional/editor/undo_spec.lua | 19 ------- test/functional/legacy/prompt_buffer_spec.lua | 54 +++++++++++++++++++ 4 files changed, 60 insertions(+), 21 deletions(-) diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 10accc4d74..55ba68f3d8 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -1615,6 +1615,8 @@ 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; + // Like submitting, undo history was relevant to the old prompt. + u_clearallandblockfree(curbuf); } curbuf->b_prompt_start.mark.col = prompt_len; curwin->w_cursor.lnum = curbuf->b_ml.ml_line_count; diff --git a/src/nvim/eval/buffer.c b/src/nvim/eval/buffer.c index f5b43fc4db..5a09020b33 100644 --- a/src/nvim/eval/buffer.c +++ b/src/nvim/eval/buffer.c @@ -792,7 +792,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); + extmark_splice_cols(buf, prompt_lno - 1, 0, old_line_len, new_prompt_len, kExtmarkNoUndo); cursor_col = new_prompt_len; } else { // Replace prev-prompt + user-input with new-prompt + user-input @@ -801,7 +801,7 @@ void f_prompt_setprompt(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) xfree(new_line); } extmark_splice_cols(buf, prompt_lno - 1, 0, buf->b_prompt_start.mark.col, new_prompt_len, - kExtmarkUndo); + kExtmarkNoUndo); cursor_col += new_prompt_len - buf->b_prompt_start.mark.col; } @@ -810,6 +810,8 @@ void f_prompt_setprompt(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) check_cursor_col(curwin); } changed_lines(buf, prompt_lno, 0, prompt_lno + 1, 0, true); + // Undo history contains the old prompt. + u_clearallandblockfree(buf); } // Clear old prompt text and replace with the new one diff --git a/test/functional/editor/undo_spec.lua b/test/functional/editor/undo_spec.lua index 0d75e072c3..2d4b79cc8c 100644 --- a/test/functional/editor/undo_spec.lua +++ b/test/functional/editor/undo_spec.lua @@ -225,22 +225,3 @@ describe(':undo! command', function() ) end) end) - -describe('undo', function() - before_each(clear) - - it('u_savecommon uses correct buffer with reload = true', function() - -- Easiest to repro in a prompt buffer. prompt_setprompt's buffer must not yet have an undo - -- header to trigger this. Will crash if it wrongly uses the unloaded curbuf in nvim_buf_call, - -- as that has no undo buffer. - eq(0, #fn.undotree().entries) - exec_lua(function() - local buf = vim.api.nvim_get_current_buf() - vim.bo.buftype = 'prompt' - vim.api.nvim_buf_call(vim.fn.bufadd(''), function() - vim.fn.prompt_setprompt(buf, 'hej > ') - end) - end) - eq('hej > ', fn.prompt_getprompt('')) - end) -end) diff --git a/test/functional/legacy/prompt_buffer_spec.lua b/test/functional/legacy/prompt_buffer_spec.lua index 9ac6ae6ac9..4986fdb0b5 100644 --- a/test/functional/legacy/prompt_buffer_spec.lua +++ b/test/functional/legacy/prompt_buffer_spec.lua @@ -514,6 +514,60 @@ describe('prompt buffer', function() {1:~ }|*3 1 line {MATCH:.*} | ]]) + + -- "S" does not clear undo + feed('ihelloS') + screen:expect([[ + cmd: tests-initial | + Command: "tests-initial" | + cmd: ^ | + {1:~ }| + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + {5:-- INSERT --} | + ]]) + feed('u') + screen:expect([[ + cmd: tests-initial | + Command: "tests-initial" | + ^cmd: hello | + {1:~ }| + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + 1 change; {MATCH:.*} | + ]]) + + -- undo cleared if prompt changes + -- (otherwise undoing would abort it and append a new prompt, which isn't useful) + fn('prompt_setprompt', '', 'cmd > ') + feed('u') + screen:expect([[ + cmd: tests-initial | + Command: "tests-initial" | + c^md > hello | + {1:~ }| + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + Already at oldest change | + ]]) + + -- new prompt line appended to fix missing prompt also clears undo + feed('A there') + fn('setpos', "':", { 0, fn('line', '.'), 99, 0 }) + feed('u') + screen:expect([[ + cmd: tests-initial | + Command: "tests-initial" | + cmd > hello there | + cmd >^ | + {3:[Prompt] [+] }| + other buffer | + {1:~ }|*3 + Already at oldest change | + ]]) end) it('o/O can create new lines', function()