commit 23aa4853b3fd3b02d5431fadeba52d97e532f53c
parent 8b7accd07e462dc6efd688fa18cb46ebb0325a67
Author: zeertzjq <zeertzjq@outlook.com>
Date: Tue, 6 Jan 2026 16:42:17 +0800
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...
Diffstat:
3 files changed, 35 insertions(+), 18 deletions(-)
diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c
@@ -671,18 +671,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)));
@@ -821,11 +809,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)) {
@@ -2076,6 +2065,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
@@ -617,7 +617,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.
@@ -2118,7 +2118,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
@@ -1018,6 +1018,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()