commit 3621af9b970c80d2a6ff36569d7495391599c334
parent 147190d8e7dd53cb329230630f764020b15d9c21
Author: zeertzjq <zeertzjq@outlook.com>
Date: Sat, 3 Jan 2026 17:55:07 +0800
fix(terminal): inconsistent mode change when switching buffer (#37215)
Problem: When switching to another terminal buffer in Terminal mode,
usually Nvim stays in Terminal mode, but leaves Terminal mode
if the old terminal buffer was deleted.
Solution: Don't always leave Terminal mode when active terminal buffer
is deleted. Instead let terminal_check_focus() decide that.
Diffstat:
2 files changed, 37 insertions(+), 3 deletions(-)
diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c
@@ -876,6 +876,11 @@ static bool terminal_check_focus(TerminalState *const s)
if (s->term != curbuf->terminal) {
// Active terminal buffer changed, flush terminal's cursor state to the UI.
terminal_focus(s->term, false);
+ if (s->close) {
+ s->term->destroy = true;
+ s->term->opts.close_cb(s->term->opts.data);
+ s->close = false;
+ }
s->term = curbuf->terminal;
s->term->pending.cursor = true;
@@ -894,6 +899,9 @@ static int terminal_check(VimState *state)
{
TerminalState *const s = (TerminalState *)state;
+ // Shouldn't reach here when pressing a key to close the terminal buffer.
+ assert(!s->close || (s->term->buf_handle == 0 && s->term != curbuf->terminal));
+
if (stop_insert_mode || !terminal_check_focus(s)) {
return 0;
}
@@ -913,7 +921,6 @@ static int terminal_check(VimState *state)
s->term->refcount--;
if (s->term->buf_handle == 0) {
s->close = true;
- return 0;
}
// Autocommands above may have changed focus, scrolled, or moved the cursor.
@@ -986,7 +993,6 @@ static int terminal_execute(VimState *state, int key)
s->term->refcount--;
if (s->term->buf_handle == 0) {
s->close = true;
- return 0;
}
break;
diff --git a/test/functional/terminal/ex_terminal_spec.lua b/test/functional/terminal/ex_terminal_spec.lua
@@ -4,11 +4,12 @@ local Screen = require('test.functional.ui.screen')
local assert_alive = n.assert_alive
local clear, poke_eventloop = n.clear, n.poke_eventloop
-local testprg, source, eq = n.testprg, n.source, t.eq
+local testprg, source, eq, neq = n.testprg, n.source, t.eq, t.neq
local feed = n.feed
local feed_command, eval = n.feed_command, n.eval
local fn = n.fn
local api = n.api
+local exec_lua = n.exec_lua
local retry = t.retry
local ok = t.ok
local command = n.command
@@ -156,6 +157,33 @@ describe(':terminal', function()
feed('<Ignore>') -- Add input to separate two RPC requests
eq({ blocking = false, mode = 'nt' }, api.nvim_get_mode())
end)
+
+ it('switching to another terminal buffer in Terminal mode', function()
+ command('terminal')
+ local buf0 = api.nvim_get_current_buf()
+ command('terminal')
+ local buf1 = api.nvim_get_current_buf()
+ command('terminal')
+ local buf2 = api.nvim_get_current_buf()
+ neq(buf0, buf1)
+ neq(buf0, buf2)
+ neq(buf1, buf2)
+ feed('i')
+ eq({ blocking = false, mode = 't' }, api.nvim_get_mode())
+ api.nvim_set_current_buf(buf1)
+ eq({ blocking = false, mode = 't' }, api.nvim_get_mode())
+ api.nvim_set_current_buf(buf0)
+ eq({ blocking = false, mode = 't' }, api.nvim_get_mode())
+ exec_lua(function()
+ vim.api.nvim_set_current_buf(buf1)
+ vim.api.nvim_buf_delete(buf0, { force = true })
+ end)
+ eq({ blocking = false, mode = 't' }, api.nvim_get_mode())
+ api.nvim_set_current_buf(buf2)
+ eq({ blocking = false, mode = 't' }, api.nvim_get_mode())
+ api.nvim_set_current_buf(buf1)
+ eq({ blocking = false, mode = 't' }, api.nvim_get_mode())
+ end)
end)
local function test_terminal_with_fake_shell(backslash)