fix(terminal): keep last cursor if it's on the last row

This commit is contained in:
zeertzjq
2025-10-28 08:10:58 +08:00
parent 3fb56ff8af
commit 4ef7aa83c4
3 changed files with 180 additions and 81 deletions

View File

@@ -3,7 +3,7 @@ local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
local tt = require('test.functional.testterm')
local clear, eq = n.clear, t.eq
local clear, eq, neq = n.clear, t.eq, t.neq
local feed, testprg = n.feed, n.testprg
local fn = n.fn
local eval = n.eval
@@ -18,34 +18,74 @@ local assert_alive = n.assert_alive
local skip = t.skip
local is_os = t.is_os
describe(':terminal scrollback', function()
local screen
local function test_terminal_scrollback(hide_curbuf)
local screen --- @type test.functional.ui.screen
local buf --- @type integer
local chan --- @type integer
local otherbuf --- @type integer
local restore_terminal_mode --- @type boolean?
local function may_hide_curbuf()
if hide_curbuf then
eq(nil, restore_terminal_mode)
restore_terminal_mode = vim.startswith(api.nvim_get_mode().mode, 't')
api.nvim_set_current_buf(otherbuf)
end
end
local function may_restore_curbuf()
if hide_curbuf then
neq(nil, restore_terminal_mode)
eq(buf, fn.bufnr('#'))
feed('<C-^>') -- "view" in 'jumpoptions' applies to this
if restore_terminal_mode then
feed('i')
else
-- Cursor position was restored from wi_mark, not b_last_cursor.
-- Check that b_last_cursor and wi_mark are the same.
local last_cursor = fn.getpos([['"]])
local restored_cursor = fn.getpos('.')
if last_cursor[2] > 0 then
eq(restored_cursor, last_cursor)
else
eq({ 0, 0, 0, 0 }, last_cursor)
eq({ 0, 1, 1, 0 }, restored_cursor)
end
end
restore_terminal_mode = nil
end
end
--- @param prefix string
--- @param start integer
--- @param stop integer
local function feed_lines(prefix, start, stop)
may_hide_curbuf()
local data = ''
for i = start, stop do
data = data .. prefix .. tostring(i) .. '\n'
end
api.nvim_chan_send(chan, data)
retry(nil, 1000, function()
eq({ prefix .. tostring(stop), '' }, api.nvim_buf_get_lines(buf, -3, -1, true))
end)
may_restore_curbuf()
end
before_each(function()
clear()
command('set nostartofline jumpoptions+=view')
screen = tt.setup_screen(nil, nil, 30)
end)
local function feed_new_lines_and_wait(count)
local lines = {}
for i = 1, count do
table.insert(lines, 'new_line' .. tostring(i))
buf = api.nvim_get_current_buf()
chan = api.nvim_get_option_value('channel', { buf = buf })
if hide_curbuf then
otherbuf = api.nvim_create_buf(true, false)
end
table.insert(lines, '')
feed_data(lines)
retry(nil, 1000, function()
eq({ 'new_line' .. tostring(count), '' }, api.nvim_buf_get_lines(0, -3, -1, true))
end)
end
end)
describe('when the limit is exceeded', function()
before_each(function()
local lines = {}
for i = 1, 30 do
table.insert(lines, 'line' .. tostring(i))
end
table.insert(lines, '')
feed_data(lines)
feed_lines('line', 1, 30)
screen:expect([[
line26 |
line27 |
@@ -87,7 +127,7 @@ describe(':terminal scrollback', function()
end)
it("when outputting fewer than 'scrollback' lines", function()
feed_new_lines_and_wait(6)
feed_lines('new_line', 1, 6)
screen:expect([[
line26 |
line27 |
@@ -102,7 +142,7 @@ describe(':terminal scrollback', function()
end)
it("when outputting more than 'scrollback' lines", function()
feed_new_lines_and_wait(11)
feed_lines('new_line', 1, 11)
screen:expect([[
line27 |
{101:line2^8} |
@@ -117,7 +157,7 @@ describe(':terminal scrollback', function()
end)
it('when outputting more lines than whole buffer', function()
feed_new_lines_and_wait(20)
feed_lines('new_line', 1, 20)
screen:expect([[
^new_line6 |
new_line7 |
@@ -150,14 +190,14 @@ describe(':terminal scrollback', function()
end)
it("when outputting fewer than 'scrollback' lines", function()
feed_new_lines_and_wait(6)
screen:expect_unchanged()
feed_lines('new_line', 1, 6)
screen:expect_unchanged(hide_curbuf)
eq({ 0, 4, 4, 0 }, fn.getpos("'m"))
eq({ 0, 4, 6, 0 }, fn.getpos('.'))
end)
it("when outputting more than 'scrollback' lines", function()
feed_new_lines_and_wait(11)
feed_lines('new_line', 1, 11)
screen:expect([[
^line27 |
line28 |
@@ -175,7 +215,7 @@ describe(':terminal scrollback', function()
describe('with cursor at last row', function()
before_each(function()
feed_data({ 'line1', 'line2', 'line3', 'line4', '' })
feed_lines('line', 1, 4)
screen:expect([[
tty ready |
line1 |
@@ -201,7 +241,7 @@ describe(':terminal scrollback', function()
it("when outputting more than 'scrollback' lines in Normal mode", function()
feed([[<C-\><C-N>]])
feed_new_lines_and_wait(11)
feed_lines('new_line', 1, 11)
screen:expect([[
new_line7 |
new_line8 |
@@ -222,11 +262,33 @@ describe(':terminal scrollback', function()
|
]])
eq({ 0, 2, 4, 0 }, fn.getpos("'m"))
feed('G')
feed_lines('new_line', 12, 31)
screen:expect([[
new_line27 |
new_line28 |
new_line29 |
new_line30 |
new_line31 |
^ |
|
]])
feed('gg')
screen:expect([[
^new_line17 |
new_line18 |
new_line19 |
new_line20 |
new_line21 |
new_line22 |
|
]])
eq({ 0, 0, 0, 0 }, fn.getpos("'m"))
end)
describe('and 1 line is printed', function()
before_each(function()
feed_data({ 'line5', '' })
feed_lines('line', 5, 5)
end)
it('will hide the top line', function()
@@ -245,7 +307,7 @@ describe(':terminal scrollback', function()
describe('and then 3 more lines are printed', function()
before_each(function()
feed_data({ 'line6', 'line7', 'line8', '' })
feed_lines('line', 6, 8)
end)
it('will hide the top 4 lines', function()
@@ -299,7 +361,9 @@ describe(':terminal scrollback', function()
describe('and height decreased by 1', function()
local function will_hide_top_line()
feed([[<C-\><C-N>]])
may_hide_curbuf()
screen:try_resize(screen._width - 2, screen._height - 1)
may_restore_curbuf()
screen:expect([[
{101:line2} |
line3 |
@@ -316,7 +380,9 @@ describe(':terminal scrollback', function()
describe('and then decreased by 2', function()
before_each(function()
will_hide_top_line()
may_hide_curbuf()
screen:try_resize(screen._width - 2, screen._height - 2)
may_restore_curbuf()
end)
it('will hide the top 3 lines', function()
@@ -357,7 +423,9 @@ describe(':terminal scrollback', function()
describe('and the height is decreased by 2', function()
before_each(function()
may_hide_curbuf()
screen:try_resize(screen._width, screen._height - 2)
may_restore_curbuf()
end)
local function will_delete_last_two_lines()
@@ -376,7 +444,9 @@ describe(':terminal scrollback', function()
describe('and then decreased by 1', function()
before_each(function()
will_delete_last_two_lines()
may_hide_curbuf()
screen:try_resize(screen._width, screen._height - 1)
may_restore_curbuf()
end)
it('will delete the last line and hide the first', function()
@@ -408,7 +478,7 @@ describe(':terminal scrollback', function()
describe('with 4 lines hidden in the scrollback', function()
before_each(function()
feed_data({ 'line1', 'line2', 'line3', 'line4', '' })
feed_lines('line', 1, 4)
screen:expect([[
tty ready |
line1 |
@@ -430,7 +500,9 @@ describe(':terminal scrollback', function()
^ |
{5:-- TERMINAL --} |
]])
may_hide_curbuf()
screen:try_resize(screen._width, screen._height - 3)
may_restore_curbuf()
screen:expect([[
line4 |
rows: 3, cols: 30 |
@@ -448,7 +520,9 @@ describe(':terminal scrollback', function()
return
end
local function pop_then_push()
may_hide_curbuf()
screen:try_resize(screen._width, screen._height + 1)
may_restore_curbuf()
screen:expect([[
line4 |
rows: 3, cols: 30 |
@@ -465,7 +539,9 @@ describe(':terminal scrollback', function()
before_each(function()
pop_then_push()
eq(8, api.nvim_buf_line_count(0))
may_hide_curbuf()
screen:try_resize(screen._width, screen._height + 3)
may_restore_curbuf()
end)
local function pop3_then_push1()
@@ -500,7 +576,9 @@ describe(':terminal scrollback', function()
before_each(function()
pop3_then_push1()
feed('Gi')
may_hide_curbuf()
screen:try_resize(screen._width, screen._height + 4)
may_restore_curbuf()
end)
it('will show all lines and leave a blank one at the end', function()
@@ -527,6 +605,55 @@ describe(':terminal scrollback', function()
end)
end)
end)
it('reducing &scrollback deletes extra lines immediately', function()
feed_lines('line', 1, 30)
screen:expect([[
line26 |
line27 |
line28 |
line29 |
line30 |
^ |
{5:-- TERMINAL --} |
]])
local term_height = 6 -- Actual terminal screen height, not the scrollback
-- Initial
local scrollback = api.nvim_get_option_value('scrollback', { buf = buf })
eq(scrollback + term_height, fn.line('$'))
eq(scrollback + term_height, fn.line('.'))
n.fn.setpos("'m", { 0, scrollback + 1, 4, 0 })
local ns = api.nvim_create_namespace('test')
api.nvim_buf_set_extmark(0, ns, scrollback, 0, { end_col = 6, hl_group = 'ErrorMsg' })
screen:expect([[
{101:line26} |
line27 |
line28 |
line29 |
line30 |
^ |
{5:-- TERMINAL --} |
]])
-- Reduction
scrollback = scrollback - 2
may_hide_curbuf()
api.nvim_set_option_value('scrollback', scrollback, { buf = buf })
may_restore_curbuf()
eq(scrollback + term_height, fn.line('$'))
eq(scrollback + term_height, fn.line('.'))
screen:expect_unchanged(hide_curbuf)
eq({ 0, scrollback + 1, 4, 0 }, n.fn.getpos("'m"))
end)
end
describe(':terminal scrollback', function()
describe('in current buffer', function()
test_terminal_scrollback(false)
end)
describe('in hidden buffer', function()
test_terminal_scrollback(true)
end)
end)
describe(':terminal prints more lines than the screen height and exits', function()
@@ -652,48 +779,6 @@ describe("'scrollback' option", function()
eq((is_os('win') and '27: line' or '26: line'), eval("getline(line('w0') - 10)->trim(' ', 2)"))
end)
it('deletes extra lines immediately', function()
-- Scrollback is 10 on setup_screen
local screen = tt.setup_screen(nil, nil, 30)
local lines = {}
for i = 1, 30 do
table.insert(lines, 'line' .. tostring(i))
end
table.insert(lines, '')
feed_data(lines)
screen:expect([[
line26 |
line27 |
line28 |
line29 |
line30 |
^ |
{5:-- TERMINAL --} |
]])
local ns = api.nvim_create_namespace('test')
local term_height = 6 -- Actual terminal screen height, not the scrollback
-- Initial
local scrollback = api.nvim_get_option_value('scrollback', {})
eq(scrollback + term_height, fn.line('$'))
n.fn.setpos("'m", { 0, scrollback + 1, 4, 0 })
api.nvim_buf_set_extmark(0, ns, scrollback, 0, { end_col = 6, hl_group = 'ErrorMsg' })
screen:expect([[
{101:line26} |
line27 |
line28 |
line29 |
line30 |
^ |
{5:-- TERMINAL --} |
]])
-- Reduction
scrollback = scrollback - 2
api.nvim_set_option_value('scrollback', scrollback, {})
eq(scrollback + term_height, fn.line('$'))
screen:expect_unchanged()
eq({ 0, scrollback + 1, 4, 0 }, n.fn.getpos("'m"))
end)
it('defaults to 10000 in :terminal buffers', function()
set_fake_shell()
command('terminal')