fix(terminal): avoid tailed cursor in focused terminal in events

Problem: in terminal mode, adjust_topline moves curwin's cursor to the last row
so set_topline tails the non-scrollback area. This may result in the observed
cursor position remaining tailed in events within the focused terminal, rather
than reflecting the actual cursor position.

Solution: use the focused terminal's actual cursor position immediately, rather
than relying on the next terminal_check_cursor call in terminal_check to set it.

Note: Maybe also possible for terminal mode cursor position to be stale
(reporting the normal mode position) when switching buffers in events to another
terminal until the next terminal_check call? (or until refresh_terminal is
called for it) Maybe not worth fixing that, though.
This commit is contained in:
Sean Dewar
2025-02-23 15:58:16 +00:00
parent 088ba835ed
commit 5e521c3b5a
3 changed files with 57 additions and 3 deletions

View File

@@ -2185,7 +2185,7 @@ void mb_adjust_cursor(void)
}
/// Checks and adjusts cursor column. Not mode-dependent.
/// @see check_cursor_col_win
/// @see check_cursor_col
///
/// @param win_ Places cursor on a valid column for this window.
void mb_check_adjust_col(void *win_)

View File

@@ -2238,8 +2238,9 @@ static void adjust_topline(Terminal *term, buf_T *buf, int added)
if (wp->w_buffer == buf) {
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 || (wp == curwin && is_focused(term))) {
if (following || focused) {
// "Follow" the terminal output
wp->w_cursor.lnum = ml_end;
set_topline(wp, MAX(wp->w_cursor.lnum - wp->w_view_height + 1, 1));
@@ -2247,9 +2248,13 @@ 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);
}
}
}
}
static int row_to_linenr(Terminal *term, int row)

View File

@@ -5,10 +5,13 @@ local tt = require('test.functional.testterm')
local feed, clear = n.feed, n.clear
local testprg, command = n.testprg, n.command
local eq, eval = t.eq, n.eval
local api = n.api
local exec_lua = n.exec_lua
local matches = t.matches
local call = n.call
local hide_cursor = tt.hide_cursor
local show_cursor = tt.show_cursor
local retry = t.retry
local is_os = t.is_os
local skip = t.skip
@@ -460,6 +463,52 @@ describe(':terminal cursor', function()
eq(0, screen._mode_info[terminal_mode_idx].blinkon)
eq(0, screen._mode_info[terminal_mode_idx].blinkoff)
end)
it('position correct within events', function()
local term, term_unfocused = exec_lua(function()
vim.cmd 'bwipeout!'
local term_unfocused = vim.api.nvim_open_term(0, {})
vim.cmd.vnew()
vim.cmd.wincmd '|'
local term = vim.api.nvim_open_term(0, {})
-- We'll use this keymap to pause the main loop while we send events, as we want the test to
-- run within the same terminal_execute call (while using test suite facilities like retry).
vim.keymap.set('t', '<F1>', '<Cmd>let g:sleepy = 1 | sleep 5000 | let g:sleepy = 0<CR>')
return term, term_unfocused
end)
feed('i<F1>')
local function check_pos(expected_pos, expected_virtcol, chan, data)
api.nvim_chan_send(chan, data) -- Using nvim_chan_send so terminal_receive is immediate.
-- Results won't be visible until refresh_terminal is called, which happens on a timer.
retry(nil, nil, function()
eq(expected_pos, eval("getpos('.')[1:]"))
end)
eq(expected_virtcol, eval("virtcol('.', 1)"))
eq(1, eval('g:sleepy')) -- :sleep shouldn't have timed out.
end
check_pos({ 1, 4, 0 }, { 4, 4 }, term, 'foo')
-- double-width char at end (3 bytes)
check_pos({ 2, 13, 0 }, { 12, 12 }, term, '\r\nbarbaaaar哦')
-- Move to 1,12 (beyond eol; sets coladd)
check_pos({ 1, 4, 8 }, { 12, 12 }, term, '\27[1;12H')
-- Move to 4,1
check_pos({ 4, 1, 0 }, { 1, 1 }, term, '\27[4;1H')
-- Move to 4,5 (beyond eol; sets coladd)
check_pos({ 4, 1, 4 }, { 5, 5 }, term, '\27[4;5H')
-- Move to 2,10 (head of wide char)
check_pos({ 2, 10, 0 }, { 10, 11 }, term, '\27[2;10H')
-- Move to 2,11 (non-head of wide char)
check_pos({ 2, 10, 0 }, { 10, 11 }, term, '\27[2;11H')
-- Move to 2,12 (after wide char)
check_pos({ 2, 13, 0 }, { 12, 12 }, term, '\27[2;12H')
-- Move to 2,13 (beyond eol; sets coladd)
check_pos({ 2, 13, 1 }, { 13, 13 }, term, '\27[2;13H')
-- Cursor movement in unfocused terminal shouldn't affect us
check_pos({ 2, 13, 1 }, { 13, 13 }, term_unfocused, 'amogus')
end)
end)
describe('buffer cursor position is correct in terminal without number column', function()