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 23aa4853b3)
This commit is contained in:
zeertzjq
2026-01-06 16:42:17 +08:00
committed by github-actions[bot]
parent ea871923eb
commit cae3c838a7
3 changed files with 35 additions and 18 deletions

View File

@@ -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());

View File

@@ -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;

View File

@@ -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([[<C-\><C-N>]])
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('<Esc>')
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([[<C-\><C-N>]])
eq({ mode = 'nt', blocking = false }, api.nvim_get_mode())
end)
end)
describe('on_lines does not emit out-of-bounds line indexes when', function()