diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index c12e01a44d..3b334963a8 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -378,6 +378,7 @@ TERMINAL • |nvim_open_term()| can be called with a non-empty buffer. The buffer contents are piped to the PTY and displayed as terminal output. +• CSI 3 J (the sequence to clear terminal scrollback) is now supported. TREESITTER @@ -433,9 +434,8 @@ These existing features changed their behavior. • 'scrollback' maximum value increased from 100000 to 1000000 • |matchfuzzy()| and |matchfuzzypos()| use an improved fuzzy matching algorithm (same as fzy). -- Windows: Paths like "\Windows" and "/Windows" are now considered to be +• Windows: Paths like "\Windows" and "/Windows" are now considered to be absolute paths (to the current drive) and no longer relative. - • When 'shelltemp' is off, shell commands now use `pipe()` and not `socketpair()` for input and output. This matters mostly for Linux where some command lines using "/dev/stdin" and similiar would break as these special files can be diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index e09427799e..e8ee47986d 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -155,9 +155,7 @@ struct terminal { size_t sb_current; // Lines stored in sb_buffer. size_t sb_size; // Capacity of sb_buffer. // "virtual index" that points to the first sb_buffer row that we need to - // push to the terminal buffer when refreshing the scrollback. When negative, - // it actually points to entries that are no longer in sb_buffer (because the - // window height has increased) and must be deleted from the terminal buffer + // push to the terminal buffer when refreshing the scrollback. int sb_pending; size_t sb_deleted; // Lines deleted from sb_buffer. size_t sb_deleted_last; // Value of sb_deleted on last refresh_scrollback() @@ -171,6 +169,7 @@ struct terminal { // refresh_timer_cb may be called after the buffer was freed, and there's // no way to know if the memory was reused. handle_T buf_handle; + bool in_altscreen; // program exited bool closed; // when true, the terminal's destruction is already enqueued. @@ -216,6 +215,7 @@ static VTermScreenCallbacks vterm_screen_callbacks = { .theme = term_theme, .sb_pushline = term_sb_push, // Called before a line goes offscreen. .sb_popline = term_sb_pop, + .sb_clear = term_sb_clear, }; static VTermSelectionCallbacks vterm_selection_callbacks = { @@ -1467,6 +1467,7 @@ static int term_settermprop(VTermProp prop, VTermValue *val, void *data) switch (prop) { case VTERM_PROP_ALTSCREEN: + term->in_altscreen = val->boolean; break; case VTERM_PROP_CURSORVISIBLE: @@ -1613,7 +1614,7 @@ static int term_sb_pop(int cols, VTermScreenCell *cells, void *data) return 0; } - if (term->sb_pending) { + if (term->sb_pending > 0) { term->sb_pending--; } @@ -1686,6 +1687,26 @@ static int term_selection_set(VTermSelectionMask mask, VTermStringFragment frag, return 1; } +static int term_sb_clear(void *data) +{ + Terminal *term = data; + + if (term->in_altscreen || !term->sb_size || !term->sb_current) { + return 1; + } + + for (size_t i = 0; i < term->sb_current; i++) { + xfree(term->sb_buffer[i]); + } + + term->sb_deleted += term->sb_current; + term->sb_current = 0; + term->sb_pending = 0; + invalidate_terminal(term, -1, -1); + + return 1; +} + // }}} // input handling {{{ @@ -2366,6 +2387,15 @@ static void refresh_scrollback(Terminal *term, buf_T *buf) int width, height; vterm_get_size(term->vt, &height, &width); + int max_line_count = (int)term->sb_current - term->sb_pending + height; + // Remove extra lines at the top if scrollback lines have been deleted. + while (deleted > 0 && buf->b_ml.ml_line_count > max_line_count) { + ml_delete_buf(buf, 1, false); + deleted_lines_buf(buf, 1, 1); + deleted--; + } + max_line_count += term->sb_pending; + // May still have pending scrollback after increase in terminal height if the // scrollback wasn't refreshed in time; append these to the top of the buffer. int row_offset = term->sb_pending; @@ -2381,21 +2411,15 @@ static void refresh_scrollback(Terminal *term, buf_T *buf) // This means that either the window height has decreased or the screen // became full and libvterm had to push all rows up. Convert the first // pending scrollback row into a string and append it just above the visible - // section of the buffer - if (((int)buf->b_ml.ml_line_count - height) >= (int)term->sb_size) { - // scrollback full, delete lines at the top - ml_delete_buf(buf, 1, false); - deleted_lines_buf(buf, 1, 1); - } + // section of the buffer. fetch_row(term, -term->sb_pending - row_offset, width); - int buf_index = (int)buf->b_ml.ml_line_count - height; + int buf_index = buf->b_ml.ml_line_count - height; ml_append_buf(buf, buf_index, term->textbuf, 0, false); appended_lines_buf(buf, buf_index, 1); term->sb_pending--; } - // Remove extra lines at the bottom - int max_line_count = (int)term->sb_current + height; + // Remove extra lines at the bottom. while (buf->b_ml.ml_line_count > max_line_count) { ml_delete_buf(buf, buf->b_ml.ml_line_count, false); deleted_lines_buf(buf, buf->b_ml.ml_line_count, 1); diff --git a/test/functional/terminal/altscreen_spec.lua b/test/functional/terminal/altscreen_spec.lua index 2216044710..40917dd0f7 100644 --- a/test/functional/terminal/altscreen_spec.lua +++ b/test/functional/terminal/altscreen_spec.lua @@ -52,12 +52,14 @@ describe(':terminal altscreen', function() line3 | |*3 ]]) + -- ED 3 is no-op in altscreen + feed_data('\027[3J') + screen:expect_unchanged() end) - describe('on exit', function() - before_each(exit_altscreen) - - it('restores buffer state', function() + describe('restores buffer state', function() + local function test_exit_altscreen_restores_buffer_state() + exit_altscreen() screen:expect([[ line4 | line5 | @@ -77,6 +79,20 @@ describe(':terminal altscreen', function() line5 | | ]]) + end + + it('after exit', function() + test_exit_altscreen_restores_buffer_state() + end) + + it('after ED 2 and ED 3 and exit', function() + feed_data('\027[H\027[2J\027[3J') + screen:expect([[ + ^ | + |*5 + {5:-- TERMINAL --} | + ]]) + test_exit_altscreen_restores_buffer_state() end) end) diff --git a/test/functional/terminal/scrollback_spec.lua b/test/functional/terminal/scrollback_spec.lua index ef0f503a5e..ceb5eef780 100644 --- a/test/functional/terminal/scrollback_spec.lua +++ b/test/functional/terminal/scrollback_spec.lua @@ -24,6 +24,7 @@ local function test_terminal_scrollback(hide_curbuf) local chan --- @type integer local otherbuf --- @type integer local restore_terminal_mode --- @type boolean? + local save_feed_data = feed_data local function may_hide_curbuf() if hide_curbuf then @@ -56,6 +57,18 @@ local function test_terminal_scrollback(hide_curbuf) end end + setup(function() + feed_data = function(data) + may_hide_curbuf() + api.nvim_chan_send(chan, data) + may_restore_curbuf() + end + end) + + teardown(function() + feed_data = save_feed_data + end) + --- @param prefix string --- @param start integer --- @param stop integer @@ -72,6 +85,12 @@ local function test_terminal_scrollback(hide_curbuf) may_restore_curbuf() end + local function try_resize(width, height) + may_hide_curbuf() + screen:try_resize(width, height) + may_restore_curbuf() + end + before_each(function() clear() command('set nostartofline jumpoptions+=view') @@ -95,6 +114,7 @@ local function test_terminal_scrollback(hide_curbuf) ^ | {5:-- TERMINAL --} | ]]) + eq(16, api.nvim_buf_line_count(0)) end) it('will delete extra lines at the top', function() @@ -126,7 +146,7 @@ local function test_terminal_scrollback(hide_curbuf) ]]) end) - it("when outputting fewer than 'scrollback' lines", function() + it("outputting fewer than 'scrollback' lines", function() feed_lines('new_line', 1, 6) screen:expect([[ line26 | @@ -141,7 +161,7 @@ local function test_terminal_scrollback(hide_curbuf) eq({ 0, 7, 6, 0 }, fn.getpos('.')) end) - it("when outputting more than 'scrollback' lines", function() + it("outputting more than 'scrollback' lines", function() feed_lines('new_line', 1, 11) screen:expect([[ line27 | @@ -156,7 +176,7 @@ local function test_terminal_scrollback(hide_curbuf) eq({ 0, 2, 6, 0 }, fn.getpos('.')) end) - it('when outputting more lines than whole buffer', function() + it('outputting more lines than whole buffer', function() feed_lines('new_line', 1, 20) screen:expect([[ ^new_line6 | @@ -170,6 +190,53 @@ local function test_terminal_scrollback(hide_curbuf) eq({ 0, 0, 0, 0 }, fn.getpos("'m")) eq({ 0, 1, 1, 0 }, fn.getpos('.')) end) + + it('clearing scrollback with ED 3', function() + feed_data('\027[3J') + screen:expect_unchanged(hide_curbuf) + eq({ 0, 3, 4, 0 }, fn.getpos("'m")) + eq({ 0, 3, 6, 0 }, fn.getpos('.')) + feed('gg') + screen:expect([[ + line2^6 | + line27 | + {101:line28} | + line29 | + line30 | + |*2 + ]]) + end) + + it('clearing scrollback with ED 3 and outputting lines', function() + feed_data('\027[3J' .. 'new_line1\nnew_line2\nnew_line3') + screen:expect([[ + line26 | + line27 | + {101:line2^8} | + line29 | + line30 | + new_line1 | + | + ]]) + eq({ 0, 3, 4, 0 }, fn.getpos("'m")) + eq({ 0, 3, 6, 0 }, fn.getpos('.')) + end) + + it('clearing scrollback with ED 3 between outputting lines', function() + skip(is_os('win'), 'FIXME: wrong behavior on Windows, ConPTY bug?') + feed_data('line31\nline32\n' .. '\027[3J' .. 'new_line1\nnew_line2') + screen:expect([[ + {101:line2^8} | + line29 | + line30 | + line31 | + line32 | + new_line1 | + | + ]]) + eq({ 0, 1, 4, 0 }, fn.getpos("'m")) + eq({ 0, 1, 6, 0 }, fn.getpos('.')) + end) end) describe('and cursor on scrollback row #12651', function() @@ -189,14 +256,14 @@ local function test_terminal_scrollback(hide_curbuf) ]]) end) - it("when outputting fewer than 'scrollback' lines", function() + it("outputting fewer than 'scrollback' lines", function() 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() + it("outputting more than 'scrollback' lines", function() feed_lines('new_line', 1, 11) screen:expect([[ ^line27 | @@ -211,6 +278,76 @@ local function test_terminal_scrollback(hide_curbuf) eq({ 0, 1, 1, 0 }, fn.getpos('.')) end) end) + + it('changing window height does not duplicate lines', function() + -- XXX: Can't test this reliably on Windows unless the cursor is _moved_ + -- by the resize. http://docs.libuv.org/en/v1.x/signal.html + -- See also: https://github.com/rprichard/winpty/issues/110 + skip(is_os('win')) + try_resize(screen._width, screen._height + 4) + screen:expect([[ + line23 | + line24 | + line25 | + line26 | + line27 | + line28 | + line29 | + line30 | + rows: 10, cols: 30 | + ^ | + {5:-- TERMINAL --} | + ]]) + eq(17, api.nvim_buf_line_count(0)) + try_resize(screen._width, screen._height - 2) + screen:expect([[ + line26 | + line27 | + line28 | + line29 | + line30 | + rows: 10, cols: 30 | + rows: 8, cols: 30 | + ^ | + {5:-- TERMINAL --} | + ]]) + eq(18, api.nvim_buf_line_count(0)) + try_resize(screen._width, screen._height - 3) + screen:expect([[ + line30 | + rows: 10, cols: 30 | + rows: 8, cols: 30 | + rows: 5, cols: 30 | + ^ | + {5:-- TERMINAL --} | + ]]) + eq(15, api.nvim_buf_line_count(0)) + try_resize(screen._width, screen._height + 3) + screen:expect([[ + line28 | + line29 | + line30 | + rows: 10, cols: 30 | + rows: 8, cols: 30 | + rows: 5, cols: 30 | + rows: 8, cols: 30 | + ^ | + {5:-- TERMINAL --} | + ]]) + eq(16, api.nvim_buf_line_count(0)) + feed([[8]]) + screen:expect([[ + line20 | + line21 | + line22 | + line23 | + line24 | + line25 | + line26 | + ^line27 | + | + ]]) + end) end) describe('with cursor at last row', function() @@ -239,7 +376,7 @@ local function test_terminal_scrollback(hide_curbuf) ]]) end) - it("when outputting more than 'scrollback' lines in Normal mode", function() + it("outputting more than 'scrollback' lines in Normal mode", function() feed([[]]) feed_lines('new_line', 1, 11) screen:expect([[ @@ -286,6 +423,79 @@ local function test_terminal_scrollback(hide_curbuf) eq({ 0, 0, 0, 0 }, fn.getpos("'m")) end) + it('clearing scrollback with ED 3', function() + -- Clearing empty scrollback and then outputting a line + feed_data('\027[3J' .. 'line5\n') + screen:expect([[ + line1 | + {101:line2} | + line3 | + line4 | + line5 | + ^ | + {5:-- TERMINAL --} | + ]]) + eq(7, api.nvim_buf_line_count(0)) + eq({ 0, 3, 4, 0 }, fn.getpos("'m")) + -- Clearing 1 line of scrollback + feed_data('\027[3J') + screen:expect_unchanged(hide_curbuf) + eq(6, api.nvim_buf_line_count(0)) + eq({ 0, 2, 4, 0 }, fn.getpos("'m")) + -- Outputting a line + feed_data('line6\n') + screen:expect([[ + {101:line2} | + line3 | + line4 | + line5 | + line6 | + ^ | + {5:-- TERMINAL --} | + ]]) + eq(7, api.nvim_buf_line_count(0)) + eq({ 0, 2, 4, 0 }, fn.getpos("'m")) + -- Clearing 1 line of scrollback and then outputting a line + feed_data('\027[3J' .. 'line7\n') + screen:expect([[ + line3 | + line4 | + line5 | + line6 | + line7 | + ^ | + {5:-- TERMINAL --} | + ]]) + eq(7, api.nvim_buf_line_count(0)) + eq({ 0, 1, 4, 0 }, fn.getpos("'m")) + -- Check first line of buffer in Normal mode + feed([[gg]]) + screen:expect([[ + {101:^line2} | + line3 | + line4 | + line5 | + line6 | + line7 | + | + ]]) + feed('G') + -- Outputting lines and then clearing scrollback + skip(is_os('win'), 'FIXME: wrong behavior on Windows, ConPTY bug?') + feed_data('line8\nline9\n' .. '\027[3J') + screen:expect([[ + line5 | + line6 | + line7 | + line8 | + line9 | + ^ | + | + ]]) + eq(6, api.nvim_buf_line_count(0)) + eq({ 0, 0, 0, 0 }, fn.getpos("'m")) + end) + describe('and 1 line is printed', function() before_each(function() feed_lines('line', 5, 5) @@ -361,9 +571,7 @@ local function test_terminal_scrollback(hide_curbuf) 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() + try_resize(screen._width - 2, screen._height - 1) screen:expect([[ {101:line2} | line3 | @@ -380,9 +588,7 @@ local function test_terminal_scrollback(hide_curbuf) 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() + try_resize(screen._width - 2, screen._height - 2) end) it('will hide the top 3 lines', function() @@ -423,9 +629,7 @@ local function test_terminal_scrollback(hide_curbuf) 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() + try_resize(screen._width, screen._height - 2) end) local function will_delete_last_two_lines() @@ -444,9 +648,7 @@ local function test_terminal_scrollback(hide_curbuf) 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() + try_resize(screen._width, screen._height - 1) end) it('will delete the last line and hide the first', function() @@ -500,9 +702,7 @@ local function test_terminal_scrollback(hide_curbuf) ^ | {5:-- TERMINAL --} | ]]) - may_hide_curbuf() - screen:try_resize(screen._width, screen._height - 3) - may_restore_curbuf() + try_resize(screen._width, screen._height - 3) screen:expect([[ line4 | rows: 3, cols: 30 | @@ -520,9 +720,7 @@ local function test_terminal_scrollback(hide_curbuf) return end local function pop_then_push() - may_hide_curbuf() - screen:try_resize(screen._width, screen._height + 1) - may_restore_curbuf() + try_resize(screen._width, screen._height + 1) screen:expect([[ line4 | rows: 3, cols: 30 | @@ -539,9 +737,7 @@ local function test_terminal_scrollback(hide_curbuf) 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() + try_resize(screen._width, screen._height + 3) end) local function pop3_then_push1() @@ -576,9 +772,7 @@ local function test_terminal_scrollback(hide_curbuf) before_each(function() pop3_then_push1() feed('Gi') - may_hide_curbuf() - screen:try_resize(screen._width, screen._height + 4) - may_restore_curbuf() + try_resize(screen._width, screen._height + 4) end) it('will show all lines and leave a blank one at the end', function() diff --git a/test/functional/terminal/window_spec.lua b/test/functional/terminal/window_spec.lua index bb6b9658cf..90c52da5ea 100644 --- a/test/functional/terminal/window_spec.lua +++ b/test/functional/terminal/window_spec.lua @@ -534,8 +534,8 @@ describe(':terminal window', function() end) command('botright new') screen:expect([[ - rows: 2, cols: 25 │rows: 5, cols: 50 | - │rows: 2, cols: 50 | + rows: 2, cols: 25 │rows: 5, cols: 25 | + │rows: 5, cols: 50 | {18:foo [-] foo [-] }| ^ | {4:~ }| @@ -545,11 +545,11 @@ describe(':terminal window', function() command('quit') eq(1, eval('g:fired')) screen:expect([[ - rows: 5, cols: 50 │rows: 5, cols: 25 | - rows: 5, cols: 25 │rows: 5, cols: 50 | - rows: 2, cols: 25 │rows: 2, cols: 50 | + rows: 5, cols: 50 │tty ready | rows: 5, cols: 25 │rows: 5, cols: 25 | - ^ │rows: 5, cols: 40 | + rows: 2, cols: 25 │rows: 5, cols: 50 | + rows: 5, cols: 25 │rows: 2, cols: 50 | + ^ │rows: 5, cols: 25 | {17:foo [-] }{18:foo [-] }| | ]]) @@ -558,14 +558,28 @@ describe(':terminal window', function() command('set showtabline=0 | tabnew | tabprevious | wincmd > | tabonly') eq(2, eval('g:fired')) screen:expect([[ - rows: 5, cols: 25 │rows: 5, cols: 25 | - rows: 2, cols: 25 │rows: 5, cols: 50 | - rows: 5, cols: 25 │rows: 2, cols: 50 | - rows: 5, cols: 26 │rows: 5, cols: 25 | - ^ │rows: 5, cols: 40 | + rows: 5, cols: 25 │tty ready | + rows: 2, cols: 25 │rows: 5, cols: 25 | + rows: 5, cols: 25 │rows: 5, cols: 50 | + rows: 5, cols: 26 │rows: 2, cols: 50 | + ^ │rows: 5, cols: 25 | {17:foo [-] }{18:foo [-] }| | ]]) + n.expect([[ + tty ready + rows: 5, cols: 25 + rows: 5, cols: 50 + rows: 2, cols: 50 + rows: 5, cols: 25 + rows: 5, cols: 40 + rows: 5, cols: 25 + rows: 5, cols: 50 + rows: 5, cols: 25 + rows: 2, cols: 25 + rows: 5, cols: 25 + rows: 5, cols: 26 + ]]) end) it('restores window options when switching terminals', function()