diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index 147ba65459..61329fc443 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -795,6 +795,11 @@ static void terminal_check_cursor(void) curwin->w_wcol = term->cursor.col + win_col_off(curwin); curwin->w_cursor.lnum = MIN(curbuf->b_ml.ml_line_count, row_to_linenr(term, term->cursor.row)); + const linenr_T topline = MAX(curbuf->b_ml.ml_line_count - curwin->w_view_height + 1, 1); + // Don't update topline if unchanged to avoid unnecessary redraws. + if (topline != curwin->w_topline) { + set_topline(curwin, topline); + } // Nudge cursor when returning to normal-mode. int off = is_focused(term) ? 0 : (curwin->w_p_rl ? 1 : -1); coladvance(curwin, MAX(0, term->cursor.col + off)); @@ -2247,11 +2252,16 @@ static void adjust_topline(Terminal *term, buf_T *buf, int added) { FOR_ALL_TAB_WINDOWS(tp, wp) { if (wp->w_buffer == buf) { + if (wp == curwin && is_focused(term)) { + // Move window cursor to terminal cursor's position and "follow" output. + terminal_check_cursor(); + continue; + } + linenr_T ml_end = buf->b_ml.ml_line_count; bool following = ml_end == wp->w_cursor.lnum + added; // cursor at end? - bool focused = wp == curwin && is_focused(term); - if (following || focused) { + if (following) { // "Follow" the terminal output wp->w_cursor.lnum = ml_end; set_topline(wp, MAX(wp->w_cursor.lnum - wp->w_view_height + 1, 1)); @@ -2259,11 +2269,7 @@ static void adjust_topline(Terminal *term, buf_T *buf, int added) // Ensure valid cursor for each window displaying this terminal. wp->w_cursor.lnum = MIN(wp->w_cursor.lnum, ml_end); } - if (focused) { - terminal_check_cursor(); - } else { - mb_check_adjust_col(wp); - } + mb_check_adjust_col(wp); } } } diff --git a/test/functional/terminal/window_spec.lua b/test/functional/terminal/window_spec.lua index b64686896d..dfe7cbd9d8 100644 --- a/test/functional/terminal/window_spec.lua +++ b/test/functional/terminal/window_spec.lua @@ -3,8 +3,10 @@ local n = require('test.functional.testnvim')() local tt = require('test.functional.testterm') local feed_data = tt.feed_data +local feed_csi = tt.feed_csi local feed, clear = n.feed, n.clear local poke_eventloop = n.poke_eventloop +local exec_lua = n.exec_lua local command = n.command local retry = t.retry local eq = t.eq @@ -332,6 +334,86 @@ describe(':terminal window', function() command('echo ""') screen:expect_unchanged() end) + + it('has correct topline if scrolled by events', function() + skip(is_os('win'), '#31587') + local lines = {} + for i = 1, 10 do + table.insert(lines, 'cool line ' .. i) + end + feed_data(lines) + feed_csi('1;1H') -- Cursor to 1,1 (after any scrollback) + + -- :sleep (with leeway) until the refresh_terminal uv timer event triggers before we move the + -- cursor. Check that the next terminal_check tails topline correctly. + command('set ruler | sleep 20m | call nvim_win_set_cursor(0, [1, 0])') + screen:expect([[ + ^cool line 5 | + cool line 6 | + cool line 7 | + cool line 8 | + cool line 9 | + cool line 10 | + {5:-- TERMINAL --} 6,1 Bot | + ]]) + command('call nvim_win_set_cursor(0, [1, 0])') + screen:expect_unchanged() + + feed_csi('2;5H') -- Cursor to 2,5 (after any scrollback) + screen:expect([[ + cool line 5 | + cool^ line 6 | + cool line 7 | + cool line 8 | + cool line 9 | + cool line 10 | + {5:-- TERMINAL --} 7,5 Bot | + ]]) + -- Check topline correct after leaving terminal mode. + -- The new cursor position is one column left of the terminal's actual cursor position. + command('stopinsert | call nvim_win_set_cursor(0, [1, 0])') + screen:expect([[ + cool line 5 | + coo^l line 6 | + cool line 7 | + cool line 8 | + cool line 9 | + cool line 10 | + 7,4 Bot | + ]]) + end) + + it('not unnecessarily redrawn by events', function() + eq('t', eval('mode()')) + exec_lua(function() + _G.redraws = {} + local ns = vim.api.nvim_create_namespace('test') + vim.api.nvim_set_decoration_provider(ns, { + on_start = function() + table.insert(_G.redraws, 'start') + end, + on_win = function(_, win) + table.insert(_G.redraws, 'win ' .. win) + end, + on_end = function() + table.insert(_G.redraws, 'end') + end, + }) + -- Setting a decoration provider typically causes an initial redraw. + vim.cmd.redraw() + _G.redraws = {} + end) + + -- The event we sent above to set up the test shouldn't have caused a redraw. + -- For good measure, also poke the event loop. + poke_eventloop() + eq({}, exec_lua('return _G.redraws')) + + -- Redraws if we do something useful, of course. + feed_data('foo') + screen:expect { any = 'foo' } + eq({ 'start', 'win 1000', 'end' }, exec_lua('return _G.redraws')) + end) end) describe(':terminal with multigrid', function()