commit 61217e361821f637a182f52da579622e63561e06
parent ad1dfc92a0fc6a15915577e2d49a6b423ca81c21
Author: Sean Dewar <6256228+seandewar@users.noreply.github.com>
Date: Fri, 28 Feb 2025 10:15:25 +0000
fix(terminal): avoid events messing up topline of focused terminal
Problem: topline of a focused terminal window may not tail to terminal output if
events scroll the window.
Solution: set the topline in terminal_check_cursor.
Diffstat:
2 files changed, 95 insertions(+), 7 deletions(-)
diff --git 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
@@ -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()