From cae3c838a707abcc73815819d9c0b4b601f2c499 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Tue, 6 Jan 2026 16:42:17 +0800 Subject: [PATCH] fix(buffer): don't reuse 1-line terminal buffer (#37261) Problem: :edit and :enew may reuse a 1-line terminal buffer, causing the new buffer to still be a terminal buffer. Solution: Don't reuse a terminal buffer, as it's not reused when it has more than 1 line. After this change close_buffer() is the only place where buf_freeall() can be called on a terminal buffer, so move the buf_close_terminal() call into buf_freeall() to save some code. Furthermore, closing the terminal in buf_freeall() is probably more correct anyway, as it is "things allocated for a buffer that are related to the file". Also, remove the useless check for on_detach callbacks deleting buffer. Even if b_locked fails to prevent that, the crash will happen at the end of buf_updates_unload() first. On the other hand, many other call sites of buf_updates_unload() and other buffer_updates_* functions don't set b_locked, which may be a problem as well... (cherry picked from commit 23aa4853b3fd3b02d5431fadeba52d97e532f53c) --- src/nvim/buffer.c | 22 ++++++------------- src/nvim/terminal.c | 4 ++-- test/functional/terminal/buffer_spec.lua | 27 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index c44166a868..aec0edbbf5 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -659,18 +659,6 @@ bool close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last, bool i buf->b_nwindows = nwindows; - if (buf->terminal) { - buf->b_locked_split++; - buf_close_terminal(buf); - buf->b_locked_split--; - - // Must check this before calling buf_freeall(), otherwise is_curbuf will be true - // in buf_freeall() but still false here, leading to a 0-line buffer. - if (buf == curbuf && !is_curbuf) { - return false; - } - } - buf_freeall(buf, ((del_buf ? BFA_DEL : 0) + (wipe_buf ? BFA_WIPE : 0) + (ignore_abort ? BFA_IGNORE_ABORT : 0))); @@ -809,11 +797,12 @@ void buf_freeall(buf_T *buf, int flags) bufref_T bufref; set_bufref(&bufref, buf); - buf_updates_unload(buf, false); - if (!bufref_valid(&bufref)) { - // on_detach callback deleted the buffer. - return; + if (buf->terminal) { + buf_close_terminal(buf); } + + buf_updates_unload(buf, false); + if ((buf->b_ml.ml_mfp != NULL) && apply_autocmds(EVENT_BUFUNLOAD, buf->b_fname, buf->b_fname, false, buf) && !bufref_valid(&bufref)) { @@ -2064,6 +2053,7 @@ bool curbuf_reusable(void) return (curbuf != NULL && curbuf->b_ffname == NULL && curbuf->b_nwindows <= 1 + && !curbuf->terminal && (curbuf->b_ml.ml_mfp == NULL || buf_is_empty(curbuf)) && !bt_quickfix(curbuf) && !curbufIsChanged()); diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index c407176142..1183e446fd 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -609,7 +609,7 @@ void terminal_close(Terminal **termpp, int status) if (status == -1 || exiting) { // If this was called by buf_close_terminal() (status is -1), or if exiting, we - // must inform the buffer the terminal no longer exists so that close_buffer() + // must inform the buffer the terminal no longer exists so that buf_freeall() // won't call buf_close_terminal() again. // If inside Terminal mode event handling, setting buf_handle to 0 also // informs terminal_enter() to call the close callback before returning. @@ -2110,7 +2110,7 @@ static void refresh_terminal(Terminal *term) { buf_T *buf = handle_get_buffer(term->buf_handle); if (!buf) { - // Destroyed by `close_buffer`. Do not do anything else. + // Destroyed by `buf_freeall()`. Do not do anything else. return; } linenr_T ml_before = buf->b_ml.ml_line_count; diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua index a0ddfa435a..7bae7a2fdf 100644 --- a/test/functional/terminal/buffer_spec.lua +++ b/test/functional/terminal/buffer_spec.lua @@ -1024,6 +1024,33 @@ describe(':terminal buffer', function() test_open_term_in_buf_with_closed_term(env) end) + + it('with nvim_open_term() channel and only 1 line is not reused by :enew', function() + command('1new') + local oldbuf = api.nvim_get_current_buf() + api.nvim_open_term(oldbuf, {}) + eq({ mode = 'nt', blocking = false }, api.nvim_get_mode()) + feed('i') + eq({ mode = 't', blocking = false }, api.nvim_get_mode()) + feed([[]]) + eq({ mode = 'nt', blocking = false }, api.nvim_get_mode()) + + command('enew') + neq(oldbuf, api.nvim_get_current_buf()) + eq({ mode = 'n', blocking = false }, api.nvim_get_mode()) + feed('i') + eq({ mode = 'i', blocking = false }, api.nvim_get_mode()) + feed('') + eq({ mode = 'n', blocking = false }, api.nvim_get_mode()) + + command('buffer #') + eq(oldbuf, api.nvim_get_current_buf()) + eq({ mode = 'nt', blocking = false }, api.nvim_get_mode()) + feed('i') + eq({ mode = 't', blocking = false }, api.nvim_get_mode()) + feed([[]]) + eq({ mode = 'nt', blocking = false }, api.nvim_get_mode()) + end) end) describe('on_lines does not emit out-of-bounds line indexes when', function()