From 16bf7652b7b204bb67921004ef2a575cf0fd160d Mon Sep 17 00:00:00 2001 From: Sean Dewar <6256228+seandewar@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:09:14 +0000 Subject: [PATCH 1/2] fix(prompt): prompt_setprompt with unloaded buffer, ': with lnum 0 Problem: prompt_setprompt memory leak/other issues when fixing prompt line for unloaded buffer, or when ': line number is zero. Solution: don't fix prompt line for unloaded buffer. Clamp ': lnum above zero. --- src/nvim/edit.c | 4 ++-- src/nvim/eval/buffer.c | 5 +++-- test/functional/legacy/prompt_buffer_spec.lua | 15 ++++++++++++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 058b36b8f4..10accc4d74 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -1594,8 +1594,8 @@ 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 = MIN(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)); 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); diff --git a/src/nvim/eval/buffer.c b/src/nvim/eval/buffer.c index b9070eb0dc..c550b6b743 100644 --- a/src/nvim/eval/buffer.c +++ b/src/nvim/eval/buffer.c @@ -772,9 +772,10 @@ void f_prompt_setprompt(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) // 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 (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 = MIN(buf->b_prompt_start.mark.lnum, buf->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)); 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 36535245ea..0e6b665310 100644 --- a/test/functional/legacy/prompt_buffer_spec.lua +++ b/test/functional/legacy/prompt_buffer_spec.lua @@ -691,12 +691,19 @@ describe('prompt buffer', function() eq(true, api.nvim_buf_set_mark(0, ':', fn('line', '.'), 999, {})) eq({ 12, 6 }, api.nvim_buf_get_mark(0, ':')) + -- Clamps lnum to at least 1. Do in one event to repro the leak. + exec_lua(function() + vim.fn.setpos("':", { 0, 0, 0, 0 }) + vim.fn.prompt_setprompt('', 'bar > ') + end) + eq({ 1, 6 }, api.nvim_buf_get_mark(0, ':')) + -- No ml_get error from invalid lnum. command('set messagesopt+=wait:0 messagesopt-=hit-enter') fn('setpos', "':", { 0, 999, 7, 0 }) eq('', api.nvim_get_vvar('errmsg')) command('set messagesopt&') - eq({ 12, 6 }, api.nvim_buf_get_mark(0, ':')) + eq({ 13, 6 }, api.nvim_buf_get_mark(0, ':')) end) describe('prompt_getinput', function() @@ -952,5 +959,11 @@ describe('prompt buffer', function() {1:~ }|*8 {5:-- INSERT --} | ]]) + + -- No leak if prompt_setprompt called for unloaded prompt buffer. + local unloaded_buf = fn('bufadd', '') + api.nvim_set_option_value('buftype', 'prompt', { buf = unloaded_buf }) + fn('prompt_setprompt', unloaded_buf, 'hello unloaded! > ') + eq('hello unloaded! > ', fn('prompt_getprompt', unloaded_buf)) end) end) From ce9dbd398b4ad31d8febde004cb7799964ecb009 Mon Sep 17 00:00:00 2001 From: Sean Dewar <6256228+seandewar@users.noreply.github.com> Date: Tue, 17 Feb 2026 00:06:03 +0000 Subject: [PATCH 2/2] fix(undo): u_savecommon uses wrong buffer Problem: u_savecommon with reload = true wrongly uses curbuf. Solution: use buf. Fix comments. --- src/nvim/undo.c | 6 +++--- test/functional/editor/undo_spec.lua | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/nvim/undo.c b/src/nvim/undo.c index e1e2238a36..67c2921a8f 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -382,7 +382,7 @@ int u_savecommon(buf_T *buf, linenr_T top, linenr_T bot, linenr_T newbot, bool r u_entry_T *prev_uep; linenr_T size = bot - top - 1; - // If curbuf->b_u_synced == true make a new header. + // If buf->b_u_synced == true make a new header. if (buf->b_u_synced) { // Need to create new entry in b_changelist. buf->b_new_change = true; @@ -401,7 +401,7 @@ int u_savecommon(buf_T *buf, linenr_T top, linenr_T bot, linenr_T newbot, bool r } // If we undid more than we redid, move the entry lists before and - // including curbuf->b_u_curhead to an alternate branch. + // including buf->b_u_curhead to an alternate branch. u_header_T *old_curhead = buf->b_u_curhead; if (old_curhead != NULL) { buf->b_u_newhead = old_curhead->uh_next.ptr; @@ -608,7 +608,7 @@ int u_savecommon(buf_T *buf, linenr_T top, linenr_T bot, linenr_T newbot, bool r buf->b_u_newhead->uh_entry = uep; if (reload) { // buffer was reloaded, notify text change subscribers - curbuf->b_u_newhead->uh_flags |= UH_RELOAD; + buf->b_u_newhead->uh_flags |= UH_RELOAD; } buf->b_u_synced = false; undo_undoes = false; diff --git a/test/functional/editor/undo_spec.lua b/test/functional/editor/undo_spec.lua index 2d4b79cc8c..0d75e072c3 100644 --- a/test/functional/editor/undo_spec.lua +++ b/test/functional/editor/undo_spec.lua @@ -225,3 +225,22 @@ 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)