fix(terminal): losing output if BufFile* poll for events (#37580)

Problem:  Terminal loses output if a BufFilePre or BufFilePost autocmd
          polls for events.
Solution: Rename the buffer after allocating the terminal instance. Also
          fix buffer getting wrong name if BufFilePre uses NameBuff.
This commit is contained in:
zeertzjq
2026-01-28 06:21:28 +08:00
committed by GitHub
parent d96a88252e
commit df21ac729c
6 changed files with 141 additions and 55 deletions

View File

@@ -1155,7 +1155,8 @@ Integer nvim_open_term(Buffer buffer, Dict(open_term) *opts, Error *err)
}
channel_incref(chan);
terminal_open(&chan->term, buf, topts);
chan->term = terminal_alloc(buf, topts);
terminal_open(&chan->term, buf);
if (chan->term != NULL) {
terminal_check_size(chan->term);
}

View File

@@ -813,11 +813,11 @@ static void channel_callback_call(Channel *chan, CallbackReader *reader)
}
}
/// Open terminal for channel
/// Allocate terminal for channel
///
/// Channel `chan` is assumed to be an open pty channel,
/// and `buf` is assumed to be a new, unmodified buffer.
void channel_terminal_open(buf_T *buf, Channel *chan)
void channel_terminal_alloc(buf_T *buf, Channel *chan)
{
TerminalOptions topts = {
.data = chan,
@@ -830,7 +830,7 @@ void channel_terminal_open(buf_T *buf, Channel *chan)
};
buf->b_p_channel = (OptInt)chan->id; // 'channel' option
channel_incref(chan);
terminal_open(&chan->term, buf, topts);
chan->term = terminal_alloc(buf, topts);
}
static void term_write(const char *buf, size_t size, void *data)

View File

@@ -3603,7 +3603,17 @@ void f_jobstart(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
return;
}
int pid = chan->stream.pty.proc.pid;
const int pid = chan->stream.pty.proc.pid;
buf_T *const buf = curbuf;
channel_incref(chan);
channel_terminal_alloc(buf, chan);
apply_autocmds(EVENT_BUFFILEPRE, NULL, NULL, false, buf);
if (chan->term == NULL || terminal_buf(chan->term) == 0) {
goto term_done; // Terminal may be destroyed during autocommands.
}
// "./…" => "/home/foo/…"
vim_FullName(cwd, NameBuff, sizeof(NameBuff), false);
@@ -3623,23 +3633,32 @@ void f_jobstart(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
// Terminal URI: "term://$CWD//$PID:$CMD"
snprintf(NameBuff, sizeof(NameBuff), "term://%s//%d:%s", IObuff, pid, cmd);
// Buffer has no terminal associated yet; unset 'swapfile' to ensure no swapfile is created.
curbuf->b_p_swf = false;
buf->b_p_swf = false;
apply_autocmds(EVENT_BUFFILEPRE, NULL, NULL, false, curbuf);
setfname(curbuf, NameBuff, NULL, true);
apply_autocmds(EVENT_BUFFILEPOST, NULL, NULL, false, curbuf);
setfname(buf, NameBuff, NULL, true);
apply_autocmds(EVENT_BUFFILEPOST, NULL, NULL, false, buf);
if (chan->term == NULL || terminal_buf(chan->term) == 0) {
goto term_done; // Terminal may be destroyed during autocommands.
}
Error err = ERROR_INIT;
buf->b_locked++;
// Set (deprecated) buffer-local vars (prefer 'channel' buffer-local option).
dict_set_var(curbuf->b_vars, cstr_as_string("terminal_job_id"),
dict_set_var(buf->b_vars, cstr_as_string("terminal_job_id"),
INTEGER_OBJ((Integer)chan->id), false, false, NULL, &err);
api_clear_error(&err);
dict_set_var(curbuf->b_vars, cstr_as_string("terminal_job_pid"),
dict_set_var(buf->b_vars, cstr_as_string("terminal_job_pid"),
INTEGER_OBJ(pid), false, false, NULL, &err);
api_clear_error(&err);
buf->b_locked--;
channel_incref(chan);
channel_terminal_open(curbuf, chan);
if (chan->term == NULL || terminal_buf(chan->term) == 0) {
goto term_done; // Terminal may be destroyed in dict watchers.
}
terminal_open(&chan->term, buf);
term_done:
channel_create_event(chan, NULL);
channel_decref(chan);
}

View File

@@ -143,7 +143,7 @@ typedef struct {
} ScrollbackLine;
struct terminal {
TerminalOptions opts; // options passed to terminal_open
TerminalOptions opts; // options passed to terminal_alloc()
VTerm *vt;
VTermScreen *vts;
// buffer used to:
@@ -474,18 +474,20 @@ static bool term_may_alloc_scrollback(Terminal *term, buf_T *buf)
// public API {{{
/// Initializes terminal properties, and triggers TermOpen.
/// Allocates a terminal instance and initializes terminal properties.
///
/// The PTY process (TerminalOptions.data) was already started by jobstart(),
/// via ex_terminal() or the term:// BufReadCmd.
///
/// @param buf Buffer used for presentation of the terminal.
/// @param opts PTY process channel, various terminal properties and callbacks.
void terminal_open(Terminal **termpp, buf_T *buf, TerminalOptions opts)
///
/// @return the terminal instance.
Terminal *terminal_alloc(buf_T *buf, TerminalOptions opts)
FUNC_ATTR_NONNULL_ALL
{
// Create a new terminal instance and configure it
Terminal *term = *termpp = xcalloc(1, sizeof(Terminal));
Terminal *term = xcalloc(1, sizeof(Terminal));
term->opts = opts;
// Associate the terminal instance with the new buffer
@@ -545,6 +547,19 @@ void terminal_open(Terminal **termpp, buf_T *buf, TerminalOptions opts)
// events from this queue are copied back onto the main event queue.
term->pending.events = multiqueue_new(NULL, NULL);
return term;
}
/// Triggers TermOpen and allocates terminal scrollback buffer.
///
/// @param termpp Pointer to the terminal channel's `term` field.
/// @param buf Buffer used for presentation of the terminal.
void terminal_open(Terminal **termpp, buf_T *buf)
FUNC_ATTR_NONNULL_ALL
{
Terminal *term = *termpp;
assert(term != NULL);
aco_save_T aco;
aucmd_prepbuf(&aco, buf);
@@ -575,6 +590,7 @@ void terminal_open(Terminal **termpp, buf_T *buf, TerminalOptions opts)
abort();
}
VTermState *state = vterm_obtain_state(term->vt);
// Configure the color palette. Try to get the color from:
//
// - b:terminal_color_{NUM}

View File

@@ -801,10 +801,9 @@ describe(':terminal buffer', function()
check_term_rep_20000('REPFAST')
end)
-- it('does not drop data when autocommands poll for events #37559', function()
it('does not drop data when TermOpen polls for events', function()
-- api.nvim_create_autocmd('BufFilePre', { command = 'sleep 50m', nested = true })
-- api.nvim_create_autocmd('BufFilePost', { command = 'sleep 50m', nested = true })
it('does not drop data when autocommands poll for events #37559', function()
api.nvim_create_autocmd('BufFilePre', { command = 'sleep 50m', nested = true })
api.nvim_create_autocmd('BufFilePost', { command = 'sleep 50m', nested = true })
api.nvim_create_autocmd('TermOpen', { command = 'sleep 50m', nested = true })
-- REP pauses 1 ms every 100 lines, so each autocommand processes some output.
check_term_rep_20000('REP')
@@ -1144,6 +1143,26 @@ describe(':terminal buffer', function()
eq('OTHER_TITLE', api.nvim_buf_get_var(0, 'term_title'))
matches('^E937: ', api.nvim_get_vvar('errmsg'))
end)
it('using NameBuff in BufFilePre does not interfere with buffer rename', function()
local oldbuf = api.nvim_get_current_buf()
n.exec([[
file Xoldfile
new Xotherfile
wincmd w
let g:BufFilePre_bufs = []
let g:BufFilePost_bufs = []
autocmd BufFilePre * call add(g:BufFilePre_bufs, [bufnr(), bufname()])
autocmd BufFilePost * call add(g:BufFilePost_bufs, [bufnr(), bufname()])
autocmd BufFilePre,BufFilePost * call execute('ls')
]])
fn.jobstart({ testprg('shell-test') }, { term = true })
eq({ { oldbuf, 'Xoldfile' } }, api.nvim_get_var('BufFilePre_bufs'))
local buffilepost_bufs = api.nvim_get_var('BufFilePost_bufs')
eq(1, #buffilepost_bufs)
eq(oldbuf, buffilepost_bufs[1][1])
matches('^term://', buffilepost_bufs[1][2])
end)
end)
describe('on_lines does not emit out-of-bounds line indexes when', function()

View File

@@ -138,41 +138,53 @@ it('chansend sends lines to terminal channel in proper order', function()
end
end)
describe('no crash when TermOpen autocommand', function()
local screen
--- @param event string
--- @param extra_tests fun(table, table)?
local function test_autocmd_no_crash(event, extra_tests)
local env = {}
-- Use REPFAST for immediately output after start.
local term_args = { testprg('shell-test'), 'REPFAST', '50', 'TEST' }
before_each(function()
clear()
screen = Screen.new(60, 4)
command([[call setline(1, 'OLDBUF') | enew]])
env.screen = Screen.new(60, 4)
command([[file Xoldbuf | call setline(1, 'OLDBUF') | enew]])
-- Wait before :bwipe to avoid closing PTY master before the child calls setsid(),
-- as that will cause SIGHUP to be also sent to the parent.
-- Use vim.uv.sleep() which blocks the event loop.
n.exec([[
func Wipe()
lua vim.uv.sleep(5)
bwipe!
endfunc
]])
end)
it('processes job exit event when using jobstart(…,{term=true})', function()
command([[autocmd TermOpen * call input('')]])
api.nvim_create_autocmd(event, { command = "call input('')" })
async_meths.nvim_call_function('jobstart', { term_args, { term = true } })
screen:expect([[
env.screen:expect([[
|
{1:~ }|*2
^ |
]])
vim.uv.sleep(20)
feed('<CR>')
screen:expect([[
env.screen:expect([[
^0: TEST |
1: TEST |
2: TEST |
|
]])
feed('i')
screen:expect([[
env.screen:expect([[
49: TEST |
|
[Process exited 0]^ |
{5:-- TERMINAL --} |
]])
feed('<CR>')
screen:expect([[
env.screen:expect([[
^OLDBUF |
{1:~ }|*2
|
@@ -181,50 +193,69 @@ describe('no crash when TermOpen autocommand', function()
end)
it('wipes buffer and processes events when using jobstart(…,{term=true})', function()
command([[autocmd TermOpen * bwipe! | call input('')]])
api.nvim_create_autocmd(event, { command = "call Wipe() | call input('')" })
async_meths.nvim_call_function('jobstart', { term_args, { term = true } })
screen:expect([[
env.screen:expect([[
|
{1:~ }|*2
^ |
]])
vim.uv.sleep(20)
feed('<CR>')
screen:expect([[
env.screen:expect([[
^OLDBUF |
{1:~ }|*2
|
]])
assert_alive()
eq('Xoldbuf', eval('bufname()'))
eq(0, eval([[exists('b:term_title')]]))
end)
it('wipes buffer and processes events when using nvim_open_term()', function()
command([[autocmd TermOpen * bwipe! | call input('')]])
async_meths.nvim_open_term(0, {})
screen:expect([[
|
{1:~ }|*2
^ |
]])
feed('<CR>')
screen:expect([[
^OLDBUF |
{1:~ }|*2
|
]])
assert_alive()
end)
if extra_tests then
extra_tests(env, term_args)
end
end
it('wipes buffer when using jobstart(…,{term=true}) during Nvim exit', function()
n.expect_exit(n.exec_lua, function()
vim.schedule(function()
vim.fn.jobstart(term_args, { term = true })
describe('no crash when TermOpen autocommand', function()
test_autocmd_no_crash('TermOpen', function(env, term_args)
it('wipes buffer and processes events when using nvim_open_term()', function()
api.nvim_create_autocmd('TermOpen', { command = "call Wipe() | call input('')" })
async_meths.nvim_open_term(0, {})
env.screen:expect([[
|
{1:~ }|*2
^ |
]])
feed('<CR>')
env.screen:expect([[
^OLDBUF |
{1:~ }|*2
|
]])
assert_alive()
end)
it('wipes buffer when using jobstart(…,{term=true}) during Nvim exit', function()
n.expect_exit(n.exec_lua, function()
vim.schedule(function()
vim.fn.jobstart(term_args, { term = true })
end)
vim.api.nvim_create_autocmd('TermOpen', { command = 'call Wipe()' })
vim.cmd('qall!')
end)
vim.cmd('autocmd TermOpen * bwipe!')
vim.cmd('qall!')
end)
end)
end)
describe('no crash when BufFilePre autocommand', function()
test_autocmd_no_crash('BufFilePre')
end)
describe('no crash when BufFilePost autocommand', function()
test_autocmd_no_crash('BufFilePost')
end)
describe('nvim_open_term', function()
local screen