diff --git a/src/nvim/mark.c b/src/nvim/mark.c index 73039642b2..48e13ffc08 100644 --- a/src/nvim/mark.c +++ b/src/nvim/mark.c @@ -1246,7 +1246,8 @@ void mark_adjust_buf(buf_T *buf, linenr_T line1, linenr_T line2, linenr_T amount ONE_ADJUST(&(buf->b_last_change.mark.lnum)); // last cursor position, if it was set - if (!equalpos(buf->b_last_cursor.mark, initpos)) { + if (!equalpos(buf->b_last_cursor.mark, initpos) + && (!by_term || buf->b_last_cursor.mark.lnum < buf->b_ml.ml_line_count)) { ONE_ADJUST(&(buf->b_last_cursor.mark.lnum)); } @@ -1364,7 +1365,9 @@ void mark_adjust_buf(buf_T *buf, linenr_T line1, linenr_T line2, linenr_T amount // adjust per-window "last cursor" positions for (size_t i = 0; i < kv_size(buf->b_wininfo); i++) { WinInfo *wip = kv_A(buf->b_wininfo, i); - ONE_ADJUST_CURSOR(&(wip->wi_mark.mark)); + if (!by_term || wip->wi_mark.mark.lnum < buf->b_ml.ml_line_count) { + ONE_ADJUST_CURSOR(&(wip->wi_mark.mark)); + } } } diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index 01c4c2197e..f859435d08 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -773,7 +773,7 @@ bool terminal_enter(void) set_terminal_winopts(s); s->term->pending.cursor = true; // Update the cursor shape table - adjust_topline(s->term, buf, 0); // scroll to end + adjust_topline_cursor(s->term, buf, 0); // scroll to end showmode(); ui_cursor_shape(); @@ -2108,7 +2108,7 @@ static void refresh_terminal(Terminal *term) refresh_screen(term, buf); int ml_added = buf->b_ml.ml_line_count - ml_before; - adjust_topline(term, buf, ml_added); + adjust_topline_cursor(term, buf, ml_added); // Copy pending events back to the main event queue multiqueue_move_events(main_loop.events, term->pending.events); @@ -2324,8 +2324,10 @@ static void refresh_screen(Terminal *term, buf_T *buf) term->invalid_end = -1; } -static void adjust_topline(Terminal *term, buf_T *buf, int added) +static void adjust_topline_cursor(Terminal *term, buf_T *buf, int added) { + linenr_T ml_end = buf->b_ml.ml_line_count; + FOR_ALL_TAB_WINDOWS(tp, wp) { if (wp->w_buffer == buf) { if (wp == curwin && is_focused(term)) { @@ -2334,9 +2336,7 @@ static void adjust_topline(Terminal *term, buf_T *buf, int added) continue; } - linenr_T ml_end = buf->b_ml.ml_line_count; bool following = ml_end == wp->w_cursor.lnum + added; // cursor at end? - if (following) { // "Follow" the terminal output wp->w_cursor.lnum = ml_end; @@ -2348,6 +2348,17 @@ static void adjust_topline(Terminal *term, buf_T *buf, int added) mb_check_adjust_col(wp); } } + + if (ml_end == buf->b_last_cursor.mark.lnum + added) { + buf->b_last_cursor.mark.lnum = ml_end; + } + + for (size_t i = 0; i < kv_size(buf->b_wininfo); i++) { + WinInfo *wip = kv_A(buf->b_wininfo, i); + if (ml_end == wip->wi_mark.mark.lnum + added) { + wip->wi_mark.mark.lnum = ml_end; + } + } } static int row_to_linenr(Terminal *term, int row) diff --git a/test/functional/terminal/scrollback_spec.lua b/test/functional/terminal/scrollback_spec.lua index bfabbf6d40..ef0f503a5e 100644 --- a/test/functional/terminal/scrollback_spec.lua +++ b/test/functional/terminal/scrollback_spec.lua @@ -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('') -- "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([[]]) - 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([[]]) + 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')