From 97d9b85bf985337b586ba8b0844866bd0210fc35 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 28 Jan 2026 23:32:58 +0800 Subject: [PATCH] fix(terminal): wrong scrollback with BufFile* autocommand (#37601) Problem: Wrong terminal scrollback if BufFile* autocommand drains PTY output but doesn't process the pending refresh. Solution: Refresh scrollback before refreshing screen in terminal_open() if scrollback has been allocated. --- src/nvim/terminal.c | 5 ++ test/functional/terminal/buffer_spec.lua | 42 +++++++++++----- test/functional/terminal/channel_spec.lua | 58 +++++++++++++++-------- 3 files changed, 72 insertions(+), 33 deletions(-) diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index 6fa495f062..1e3b7eb8d4 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -563,6 +563,11 @@ void terminal_open(Terminal **termpp, buf_T *buf) aco_save_T aco; aucmd_prepbuf(&aco, buf); + if (term->sb_buffer != NULL) { + // If scrollback has been allocated by autocommands between terminal_alloc() + // and terminal_open(), it also needs to be refreshed. + refresh_scrollback(term, buf); + } refresh_screen(term, buf); set_option_value(kOptBuftype, STATIC_CSTR_AS_OPTVAL("terminal"), OPT_LOCAL); diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua index 6f65fdc77c..a8038d2b3d 100644 --- a/test/functional/terminal/buffer_spec.lua +++ b/test/functional/terminal/buffer_spec.lua @@ -773,40 +773,56 @@ describe(':terminal buffer', function() end) --- @param subcmd 'REP'|'REPFAST' - local function check_term_rep_20000(subcmd) + local function check_term_rep(subcmd, count) local screen = Screen.new(50, 7) - api.nvim_set_option_value('scrollback', 30000, {}) api.nvim_create_autocmd('TermClose', { command = 'let g:did_termclose = 1' }) - fn.jobstart({ testprg('shell-test'), subcmd, '20000', 'TEST' }, { term = true }) + fn.jobstart({ testprg('shell-test'), subcmd, count, 'TEST' }, { term = true }) retry(nil, nil, function() eq(1, api.nvim_get_var('did_termclose')) end) feed('i') - screen:expect([[ - 19996: TEST | - 19997: TEST | - 19998: TEST | - 19999: TEST | + screen:expect(([[ + %d: TEST{MATCH: +}| + %d: TEST{MATCH: +}| + %d: TEST{MATCH: +}| + %d: TEST{MATCH: +}| | [Process exited 0]^ | {5:-- TERMINAL --} | - ]]) + ]]):format(count - 4, count - 3, count - 2, count - 1)) local lines = api.nvim_buf_get_lines(0, 0, -1, true) - for i = 0, 19999 do - eq(('%d: TEST'):format(i), lines[i + 1]) + for i = 1, count do + eq(('%d: TEST'):format(i - 1), lines[i]) end end it('does not drop data when job exits immediately after output #3030', function() - check_term_rep_20000('REPFAST') + api.nvim_set_option_value('scrollback', 30000, {}) + check_term_rep('REPFAST', 20000) end) it('does not drop data when autocommands poll for events #37559', function() + api.nvim_set_option_value('scrollback', 30000, {}) 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') + check_term_rep('REP', 20000) + end) + + describe('scrollback is correct if all output is drained by', function() + for _, event in ipairs({ 'BufFilePre', 'BufFilePost', 'TermOpen' }) do + describe(('%s autocommand that lasts for'):format(event), function() + for _, delay in ipairs({ 5, 15, 25 }) do + -- Terminal refresh delay is 10 ms. + it(('%.1f * terminal refresh delay'):format(delay / 10), function() + local cmd = ('sleep %dm'):format(delay) + api.nvim_create_autocmd(event, { command = cmd, nested = true }) + check_term_rep('REPFAST', 200) + end) + end + end) + end end) it('handles unprintable chars', function() diff --git a/test/functional/terminal/channel_spec.lua b/test/functional/terminal/channel_spec.lua index 7f93096e3f..418c86b08d 100644 --- a/test/functional/terminal/channel_spec.lua +++ b/test/functional/terminal/channel_spec.lua @@ -160,14 +160,21 @@ local function test_autocmd_no_crash(event, extra_tests) ]]) end) + local input_prompt_screen = [[ + | + {1:~ }|*2 + ^ | + ]] + local oldbuf_screen = [[ + ^OLDBUF | + {1:~ }|*2 + | + ]] + it('processes job exit event when using jobstart(…,{term=true})', function() api.nvim_create_autocmd(event, { command = "call input('')" }) async_meths.nvim_call_function('jobstart', { term_args, { term = true } }) - env.screen:expect([[ - | - {1:~ }|*2 - ^ | - ]]) + env.screen:expect(input_prompt_screen) vim.uv.sleep(20) feed('') env.screen:expect([[ @@ -184,29 +191,40 @@ local function test_autocmd_no_crash(event, extra_tests) {5:-- TERMINAL --} | ]]) feed('') - env.screen:expect([[ - ^OLDBUF | - {1:~ }|*2 - | - ]]) + env.screen:expect(oldbuf_screen) assert_alive() end) it('wipes buffer and processes events when using jobstart(…,{term=true})', function() api.nvim_create_autocmd(event, { command = "call Wipe() | call input('')" }) async_meths.nvim_call_function('jobstart', { term_args, { term = true } }) - env.screen:expect([[ - | - {1:~ }|*2 - ^ | - ]]) + env.screen:expect(input_prompt_screen) vim.uv.sleep(20) feed('') - env.screen:expect([[ - ^OLDBUF | - {1:~ }|*2 - | - ]]) + env.screen:expect(oldbuf_screen) + assert_alive() + eq('Xoldbuf', eval('bufname()')) + eq(0, eval([[exists('b:term_title')]])) + end) + + it('processes :bwipe from TermClose when using jobstart(…,{term=true})', function() + local term_buf = api.nvim_get_current_buf() + api.nvim_create_autocmd('TermClose', { command = ('bwipe! %d'):format(term_buf) }) + api.nvim_create_autocmd(event, { command = "call input('')", nested = true }) + async_meths.nvim_call_function('jobstart', { term_args, { term = true } }) + env.screen:expect(input_prompt_screen) + vim.uv.sleep(20) + feed('') + env.screen:expect(oldbuf_screen) + assert_alive() + eq('Xoldbuf', eval('bufname()')) + eq(0, eval([[exists('b:term_title')]])) + end) + + it('only wipes buffer when using jobstart(…,{term=true})', function() + api.nvim_create_autocmd(event, { command = 'call Wipe()' }) + async_meths.nvim_call_function('jobstart', { term_args, { term = true } }) + env.screen:expect(oldbuf_screen) assert_alive() eq('Xoldbuf', eval('bufname()')) eq(0, eval([[exists('b:term_title')]]))