Merge pull request #36379 from zeertzjq/backport

Backport terminal fixes
This commit is contained in:
zeertzjq
2025-10-29 13:43:40 +08:00
committed by GitHub
12 changed files with 1159 additions and 204 deletions

View File

@@ -408,7 +408,7 @@ struct file_buffer {
varnumber_T b_last_changedtick; // b:changedtick when TextChanged was
// last triggered.
varnumber_T b_last_changedtick_i; // b:changedtick for TextChangedI
varnumber_T b_last_changedtick_i; // b:changedtick for TextChangedI/T
varnumber_T b_last_changedtick_pum; // b:changedtick for TextChangedP
bool b_saving; // Set to true if we are in the middle of

View File

@@ -1158,6 +1158,23 @@ void ex_changes(exarg_T *eap)
} \
}
// Like ONE_ADJUST_NODEL(), but if the position is within the deleted range,
// move it to the start of the line before the range.
#define ONE_ADJUST_CURSOR(pp) \
{ \
pos_T *posp = pp; \
if (posp->lnum >= line1 && posp->lnum <= line2) { \
if (amount == MAXLNUM) { /* line with cursor is deleted */ \
posp->lnum = MAX(line1 - 1, 1); \
posp->col = 0; \
} else { /* keep cursor on the same line */ \
posp->lnum += amount; \
} \
} else if (amount_after && posp->lnum > line2) { \
posp->lnum += amount_after; \
} \
}
// Adjust marks between "line1" and "line2" (inclusive) to move "amount" lines.
// Must be called before changed_*(), appended_lines() or deleted_lines().
// May be called before or after changing the text.
@@ -1219,7 +1236,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));
}
@@ -1316,20 +1334,7 @@ void mark_adjust_buf(buf_T *buf, linenr_T line1, linenr_T line2, linenr_T amount
}
}
if (!by_api && (by_term ? win->w_cursor.lnum < buf->b_ml.ml_line_count : win != curwin)) {
if (win->w_cursor.lnum >= line1 && win->w_cursor.lnum <= line2) {
if (amount == MAXLNUM) { // line with cursor is deleted
if (line1 <= 1) {
win->w_cursor.lnum = 1;
} else {
win->w_cursor.lnum = line1 - 1;
}
win->w_cursor.col = 0;
} else { // keep cursor on the same line
win->w_cursor.lnum += amount;
}
} else if (amount_after && win->w_cursor.lnum > line2) {
win->w_cursor.lnum += amount_after;
}
ONE_ADJUST_CURSOR(&(win->w_cursor));
}
if (adjust_folds) {
@@ -1340,6 +1345,14 @@ void mark_adjust_buf(buf_T *buf, linenr_T line1, linenr_T line2, linenr_T amount
// adjust diffs
diff_mark_adjust(buf, line1, line2, amount, amount_after);
// 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);
if (!by_term || wip->wi_mark.mark.lnum < buf->b_ml.ml_line_count) {
ONE_ADJUST_CURSOR(&(wip->wi_mark.mark));
}
}
}
// This code is used often, needs to be fast.

View File

@@ -2187,7 +2187,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

@@ -112,6 +112,16 @@ typedef struct {
bool got_bsl; ///< if the last input was <C-\>
bool got_bsl_o; ///< if left terminal mode with <c-\><c-o>
bool cursor_visible; ///< cursor's current visibility; ensures matched busy_start/stop UI events
// These fields remember the prior values of window options before entering terminal mode.
// Valid only when save_curwin_handle != 0.
handle_T save_curwin_handle;
bool save_w_p_cul;
char *save_w_p_culopt;
uint8_t save_w_p_culopt_flags;
int save_w_p_cuc;
OptInt save_w_p_so;
OptInt save_w_p_siso;
} TerminalState;
#ifdef INCLUDE_GENERATED_DECLARATIONS
@@ -596,14 +606,14 @@ void terminal_close(Terminal **termpp, int status)
// If this was called by close_buffer() (status is -1), or if exiting, we
// must inform the buffer the terminal no longer exists so that
// close_buffer() won't call this again.
// If inside Terminal mode K_EVENT handling, setting buf_handle to 0 also
// If inside Terminal mode event handling, setting buf_handle to 0 also
// informs terminal_enter() to call the close callback before returning.
term->buf_handle = 0;
if (buf) {
buf->terminal = NULL;
}
if (!term->refcount) {
// Not inside Terminal mode K_EVENT handling.
// Not inside Terminal mode event handling.
// We should not wait for the user to press a key.
term->destroy = true;
term->opts.close_cb(term->opts.data);
@@ -671,6 +681,75 @@ void terminal_check_size(Terminal *term)
invalidate_terminal(term, -1, -1);
}
static void set_terminal_winopts(TerminalState *const s)
FUNC_ATTR_NONNULL_ALL
{
assert(s->save_curwin_handle == 0);
// Disable these options in terminal-mode. They are nonsense because cursor is
// placed at end of buffer to "follow" output. #11072
s->save_curwin_handle = curwin->handle;
s->save_w_p_cul = curwin->w_p_cul;
s->save_w_p_culopt = NULL;
s->save_w_p_culopt_flags = curwin->w_p_culopt_flags;
s->save_w_p_cuc = curwin->w_p_cuc;
s->save_w_p_so = curwin->w_p_so;
s->save_w_p_siso = curwin->w_p_siso;
if (curwin->w_p_cul && curwin->w_p_culopt_flags & kOptCuloptFlagNumber) {
if (!strequal(curwin->w_p_culopt, "number")) {
s->save_w_p_culopt = curwin->w_p_culopt;
curwin->w_p_culopt = xstrdup("number");
}
curwin->w_p_culopt_flags = kOptCuloptFlagNumber;
} else {
curwin->w_p_cul = false;
}
curwin->w_p_cuc = false;
curwin->w_p_so = 0;
curwin->w_p_siso = 0;
if (curwin->w_p_cuc != s->save_w_p_cuc) {
redraw_later(curwin, UPD_SOME_VALID);
} else if (curwin->w_p_cul != s->save_w_p_cul
|| (curwin->w_p_cul && curwin->w_p_culopt_flags != s->save_w_p_culopt_flags)) {
redraw_later(curwin, UPD_VALID);
}
}
static void unset_terminal_winopts(TerminalState *const s)
FUNC_ATTR_NONNULL_ALL
{
assert(s->save_curwin_handle != 0);
win_T *const wp = handle_get_window(s->save_curwin_handle);
if (!wp) {
free_string_option(s->save_w_p_culopt);
s->save_curwin_handle = 0;
return;
}
if (win_valid(wp)) { // No need to redraw if window not in curtab.
if (s->save_w_p_cuc != wp->w_p_cuc) {
redraw_later(wp, UPD_SOME_VALID);
} else if (s->save_w_p_cul != wp->w_p_cul
|| (s->save_w_p_cul && s->save_w_p_culopt_flags != wp->w_p_culopt_flags)) {
redraw_later(wp, UPD_VALID);
}
}
wp->w_p_cul = s->save_w_p_cul;
if (s->save_w_p_culopt) {
free_string_option(wp->w_p_culopt);
wp->w_p_culopt = s->save_w_p_culopt;
}
wp->w_p_culopt_flags = s->save_w_p_culopt_flags;
wp->w_p_cuc = s->save_w_p_cuc;
wp->w_p_so = s->save_w_p_so;
wp->w_p_siso = s->save_w_p_siso;
s->save_curwin_handle = 0;
}
/// Implements MODE_TERMINAL state. :help Terminal-mode
bool terminal_enter(void)
{
@@ -692,42 +771,20 @@ bool terminal_enter(void)
mapped_ctrl_c |= MODE_TERMINAL; // Always map CTRL-C to avoid interrupt.
RedrawingDisabled = false;
// Disable these options in terminal-mode. They are nonsense because cursor is
// placed at end of buffer to "follow" output. #11072
handle_T save_curwin = curwin->handle;
bool save_w_p_cul = curwin->w_p_cul;
char *save_w_p_culopt = NULL;
uint8_t save_w_p_culopt_flags = curwin->w_p_culopt_flags;
int save_w_p_cuc = curwin->w_p_cuc;
OptInt save_w_p_so = curwin->w_p_so;
OptInt save_w_p_siso = curwin->w_p_siso;
if (curwin->w_p_cul && curwin->w_p_culopt_flags & kOptCuloptFlagNumber) {
if (strcmp(curwin->w_p_culopt, "number") != 0) {
save_w_p_culopt = curwin->w_p_culopt;
curwin->w_p_culopt = xstrdup("number");
}
curwin->w_p_culopt_flags = kOptCuloptFlagNumber;
} else {
curwin->w_p_cul = false;
}
if (curwin->w_p_cuc) {
redraw_later(curwin, UPD_SOME_VALID);
}
curwin->w_p_cuc = false;
curwin->w_p_so = 0;
curwin->w_p_siso = 0;
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();
curwin->w_redr_status = true; // For mode() in statusline. #8323
redraw_custom_title_later();
ui_cursor_shape();
apply_autocmds(EVENT_TERMENTER, NULL, NULL, false, curbuf);
may_trigger_modechanged();
// Tell the terminal it has focus
terminal_focus(s->term, true);
// Don't fire TextChangedT from changes in Normal mode.
curbuf->b_last_changedtick_i = buf_get_changedtick(curbuf);
apply_autocmds(EVENT_TERMENTER, NULL, NULL, false, curbuf);
may_trigger_modechanged();
s->state.execute = terminal_execute;
s->state.check = terminal_check;
@@ -743,27 +800,15 @@ bool terminal_enter(void)
ui_busy_stop();
}
apply_autocmds(EVENT_TERMLEAVE, NULL, NULL, false, curbuf);
// Restore the terminal cursor to what is set in 'guicursor'
(void)parse_shape_opt(SHAPE_CURSOR);
if (save_curwin == curwin->handle) { // Else: window was closed.
curwin->w_p_cul = save_w_p_cul;
if (save_w_p_culopt) {
free_string_option(curwin->w_p_culopt);
curwin->w_p_culopt = save_w_p_culopt;
}
curwin->w_p_culopt_flags = save_w_p_culopt_flags;
curwin->w_p_cuc = save_w_p_cuc;
curwin->w_p_so = save_w_p_so;
curwin->w_p_siso = save_w_p_siso;
} else if (save_w_p_culopt) {
free_string_option(save_w_p_culopt);
}
unset_terminal_winopts(s);
// Tell the terminal it lost focus
terminal_focus(s->term, false);
// Don't fire TextChanged from changes in terminal mode.
curbuf->b_last_changedtick = buf_get_changedtick(curbuf);
if (curbuf->terminal == s->term && !s->close) {
terminal_check_cursor();
@@ -774,12 +819,19 @@ bool terminal_enter(void)
unshowmode(true);
}
ui_cursor_shape();
// If we're to close the terminal, don't let TermLeave autocommands free it first!
if (s->close) {
bool wipe = s->term->buf_handle != 0;
s->term->refcount++;
}
apply_autocmds(EVENT_TERMLEAVE, NULL, NULL, false, curbuf);
if (s->close) {
s->term->refcount--;
const handle_T buf_handle = s->term->buf_handle; // Callback may free s->term.
s->term->destroy = true;
s->term->opts.close_cb(s->term->opts.data);
if (wipe) {
do_cmdline_cmd("bwipeout!");
if (buf_handle != 0) {
do_buffer(DOBUF_WIPE, DOBUF_FIRST, FORWARD, buf_handle, true);
}
}
@@ -789,51 +841,88 @@ bool terminal_enter(void)
static void terminal_check_cursor(void)
{
Terminal *term = curbuf->terminal;
curwin->w_wrow = term->cursor.row;
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_height_inner + 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));
}
// Function executed before each iteration of terminal mode.
// Return:
// 1 if the iteration should continue normally
// 0 if the main loop must exit
static bool terminal_check_focus(TerminalState *const s)
FUNC_ATTR_NONNULL_ALL
{
if (curbuf->terminal == NULL) {
return false;
}
if (s->save_curwin_handle != curwin->handle) {
// Terminal window changed, update window options.
unset_terminal_winopts(s);
set_terminal_winopts(s);
}
if (s->term != curbuf->terminal) {
// Active terminal buffer changed, flush terminal's cursor state to the UI.
terminal_focus(s->term, false);
s->term = curbuf->terminal;
s->term->pending.cursor = true;
invalidate_terminal(s->term, -1, -1);
terminal_focus(s->term, true);
}
return true;
}
/// Function executed before each iteration of terminal mode.
///
/// @return:
/// 1 if the iteration should continue normally
/// 0 if the main loop must exit
static int terminal_check(VimState *state)
{
TerminalState *const s = (TerminalState *)state;
if (stop_insert_mode) {
if (stop_insert_mode || !terminal_check_focus(s)) {
return 0;
}
assert(s->term == curbuf->terminal);
// Validate topline and cursor position for autocommands. Especially important for WinScrolled.
terminal_check_cursor();
validate_cursor(curwin);
if (must_redraw) {
update_screen();
// Make sure an invoked autocmd doesn't delete the buffer (and the
// terminal) under our fingers.
curbuf->b_locked++;
// save and restore curwin and curbuf, in case the autocmd changes them
aco_save_T aco;
aucmd_prepbuf(&aco, curbuf);
// Don't let autocommands free the terminal from under our fingers.
s->term->refcount++;
if (has_event(EVENT_TEXTCHANGEDT)
&& curbuf->b_last_changedtick_i != buf_get_changedtick(curbuf)) {
apply_autocmds(EVENT_TEXTCHANGEDT, NULL, NULL, false, curbuf);
aucmd_restbuf(&aco);
curbuf->b_locked--;
curbuf->b_last_changedtick_i = buf_get_changedtick(curbuf);
}
may_trigger_win_scrolled_resized();
s->term->refcount--;
if (s->term->buf_handle == 0) {
s->close = true;
return 0;
}
may_trigger_win_scrolled_resized();
// Autocommands above may have changed focus, scrolled, or moved the cursor.
if (!terminal_check_focus(s)) {
return 0;
}
terminal_check_cursor();
validate_cursor(curwin);
if (need_maketitle) { // Update title in terminal-mode. #7248
maketitle();
show_cursor_info_later(false);
if (must_redraw) {
update_screen();
} else {
redraw_statuslines();
if (clear_cmdline || redraw_cmdline || redraw_mode) {
showmode(); // clear cmdline and show mode
}
}
setcursor();
@@ -935,15 +1024,6 @@ static int terminal_execute(VimState *state, int key)
terminal_send_key(s->term, key);
}
if (curbuf->terminal == NULL) {
return 0;
}
if (s->term != curbuf->terminal) {
// Active terminal buffer changed, flush terminal's cursor state to the UI
s->term = curbuf->terminal;
s->term->pending.cursor = true;
invalidate_terminal(s->term, -1, -1);
}
return 1;
}
@@ -2029,7 +2109,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);
@@ -2245,14 +2325,20 @@ 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) {
linenr_T ml_end = buf->b_ml.ml_line_count;
bool following = ml_end == wp->w_cursor.lnum + added; // cursor at end?
if (wp == curwin && is_focused(term)) {
// Move window cursor to terminal cursor's position and "follow" output.
terminal_check_cursor();
continue;
}
if (following || (wp == curwin && is_focused(term))) {
bool following = ml_end == wp->w_cursor.lnum + added; // cursor at end?
if (following) {
// "Follow" the terminal output
wp->w_cursor.lnum = ml_end;
set_topline(wp, MAX(wp->w_cursor.lnum - wp->w_height_inner + 1, 1));
@@ -2263,6 +2349,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)

View File

@@ -4245,6 +4245,10 @@ int win_new_tabpage(int after, char *filename)
newtp->tp_topframe = topframe;
last_status(false);
if (curbuf->terminal) {
terminal_check_size(curbuf->terminal);
}
redraw_all_later(UPD_NOT_VALID);
tabpage_check_windows(old_curtab);

View File

@@ -1,10 +1,11 @@
local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local tt = require('test.functional.testterm')
local Screen = require('test.functional.ui.screen')
local uv = vim.uv
local clear, command, testprg = n.clear, n.command, n.testprg
local eval, eq, neq, retry = n.eval, t.eq, t.neq, t.retry
local exec_lua = n.exec_lua
local matches = t.matches
local ok = t.ok
local feed = n.feed
@@ -197,30 +198,132 @@ it('autocmd TermEnter, TermLeave', function()
}, eval('g:evs'))
end)
describe('autocmd TextChangedT', function()
local screen
before_each(function()
clear()
screen = tt.setup_screen()
end)
describe('autocmd TextChangedT,WinResized', function()
before_each(clear)
it('works', function()
command('autocmd TextChangedT * ++once let g:called = 1')
tt.feed_data('a')
retry(nil, nil, function()
eq(1, api.nvim_get_var('called'))
it('TextChangedT works', function()
local screen = Screen.new(50, 7)
screen:set_default_attr_ids({
[1] = { bold = true },
[31] = { foreground = Screen.colors.Gray100, background = Screen.colors.DarkGreen },
[32] = {
foreground = Screen.colors.Gray100,
bold = true,
background = Screen.colors.DarkGreen,
},
})
local term, term_unfocused = exec_lua(function()
-- Split windows before opening terminals so TextChangedT doesn't fire an additional time due
-- to the inner terminal being resized (which is usually deferred too).
vim.cmd.vnew()
local term_unfocused = vim.api.nvim_open_term(0, {})
vim.cmd.wincmd 'p'
local term = vim.api.nvim_open_term(0, {})
vim.cmd.startinsert()
return term, term_unfocused
end)
eq('t', eval('mode()'))
exec_lua(function()
_G.n_triggered = 0
vim.api.nvim_create_autocmd('TextChanged', {
callback = function()
_G.n_triggered = _G.n_triggered + 1
end,
})
_G.t_triggered = 0
vim.api.nvim_create_autocmd('TextChangedT', {
callback = function()
_G.t_triggered = _G.t_triggered + 1
end,
})
end)
api.nvim_chan_send(term, 'a')
retry(nil, nil, function()
eq(1, exec_lua('return _G.t_triggered'))
end)
api.nvim_chan_send(term, 'b')
retry(nil, nil, function()
eq(2, exec_lua('return _G.t_triggered'))
end)
-- Not triggered by changes in a non-current terminal.
api.nvim_chan_send(term_unfocused, 'hello')
screen:expect([[
hello │ab^ |
│ |*4
{31:[Scratch] }{32:[Scratch] }|
{1:-- TERMINAL --} |
]])
eq(2, exec_lua('return _G.t_triggered'))
-- Not triggered by unflushed redraws.
api.nvim__redraw({ valid = false, flush = false })
eq(2, exec_lua('return _G.t_triggered'))
-- Not triggered when not in terminal mode.
command('stopinsert')
eq('n', eval('mode()'))
eq(2, exec_lua('return _G.t_triggered'))
eq(0, exec_lua('return _G.n_triggered')) -- Nothing we did was in Normal mode yet.
api.nvim_chan_send(term, 'c')
screen:expect([[
hello │a^bc |
│ |*4
{31:[Scratch] }{32:[Scratch] }|
|
]])
eq(1, exec_lua('return _G.n_triggered')) -- Happened in Normal mode.
end)
it('cannot delete terminal buffer', function()
command('autocmd TextChangedT * bwipe!')
tt.feed_data('a')
screen:expect({ any = 'E937: ' })
feed('<CR>')
command('autocmd! TextChangedT')
matches(
'^E937: Attempt to delete a buffer that is in use: term://',
api.nvim_get_vvar('errmsg')
)
it('no crash when deleting terminal buffer', function()
-- Using nvim_open_term over :terminal as the former can free the terminal immediately on
-- close, causing the crash.
-- WinResized
local buf1, term1 = exec_lua(function()
vim.cmd.new()
local buf = vim.api.nvim_get_current_buf()
local term = vim.api.nvim_open_term(0, {
on_input = function()
vim.cmd.wincmd '_'
end,
})
vim.api.nvim_create_autocmd('WinResized', {
once = true,
command = 'bwipeout!',
})
return buf, term
end)
feed('ii')
eq(false, api.nvim_buf_is_valid(buf1))
eq('n', eval('mode()'))
eq({}, api.nvim_get_chan_info(term1)) -- Channel should've been cleaned up.
-- TextChangedT
local buf2, term2 = exec_lua(function()
vim.cmd.new()
local buf = vim.api.nvim_get_current_buf()
local term = vim.api.nvim_open_term(0, {
on_input = function(_, chan)
vim.api.nvim_chan_send(chan, 'sup')
end,
})
vim.api.nvim_create_autocmd('TextChangedT', {
once = true,
command = 'bwipeout!',
})
return buf, term
end)
feed('ii')
-- refresh_terminal is deferred, so TextChangedT may not trigger immediately.
retry(nil, nil, function()
eq(false, api.nvim_buf_is_valid(buf2))
end)
eq('n', eval('mode()'))
eq({}, api.nvim_get_chan_info(term2)) -- Channel should've been cleaned up.
end)
end)

View File

@@ -343,6 +343,94 @@ describe(':terminal buffer', function()
|*4
]])
end)
it('reports focus notifications when requested', function()
feed([[<C-\><C-N>]])
exec_lua(function()
local function new_test_term()
local chan = vim.api.nvim_open_term(0, {
on_input = function(_, term, buf, data)
if data == '\27[I' then
vim.b[buf].term_focused = true
vim.api.nvim_chan_send(term, 'focused\n')
elseif data == '\27[O' then
vim.b[buf].term_focused = false
vim.api.nvim_chan_send(term, 'unfocused\n')
end
end,
})
vim.b.term_focused = false
vim.api.nvim_chan_send(chan, '\27[?1004h') -- Enable focus reporting
end
vim.cmd 'edit bar'
new_test_term()
vim.cmd 'vnew foo'
new_test_term()
vim.cmd 'vsplit'
end)
screen:expect([[
^ │ │ |
│ │ |*4
{17:foo }{18:foo bar }|
|
]])
-- TermEnter/Leave happens *after* entering/leaving terminal mode, so focus should've changed
-- already by the time these events run.
exec_lua(function()
_G.last_event = nil
vim.api.nvim_create_autocmd({ 'TermEnter', 'TermLeave' }, {
callback = function(args)
_G.last_event = args.event
.. ' '
.. vim.fs.basename(args.file)
.. ' '
.. tostring(vim.b[args.buf].term_focused)
end,
})
end)
feed('i')
screen:expect([[
focused │focused │ |
^ │ │ |
│ │ |*3
{17:foo }{18:foo bar }|
{3:-- TERMINAL --} |
]])
eq('TermEnter foo true', exec_lua('return _G.last_event'))
-- Next window has the same terminal; no new notifications.
command('wincmd w')
screen:expect([[
focused │focused │ |
│^ │ |
│ │ |*3
{18:foo }{17:foo }{18:bar }|
{3:-- TERMINAL --} |
]])
-- Next window has a different terminal; expect new unfocus and focus notifications.
command('wincmd w')
screen:expect([[
focused │focused │focused |
unfocused │unfocuse│^ |
│ │ |*3
{18:foo foo }{17:bar }|
{3:-- TERMINAL --} |
]])
-- Leaving terminal mode; expect a new unfocus notification.
feed([[<C-\><C-N>]])
screen:expect([[
focused │focused │focused |
unfocused │unfocuse│unfocused |
│ │^ |
│ │ |*2
{18:foo foo }{17:bar }|
|
]])
eq('TermLeave bar false', exec_lua('return _G.last_event'))
end)
end)
describe(':terminal buffer', function()
@@ -684,6 +772,58 @@ describe(':terminal buffer', function()
unchanged = true,
})
end)
it('does not wipeout unrelated buffer after channel closes', function()
local screen = Screen.new(50, 7)
screen:set_default_attr_ids({
[1] = { foreground = Screen.colors.Blue1, bold = true },
[2] = { reverse = true },
[31] = { background = Screen.colors.DarkGreen, foreground = Screen.colors.White, bold = true },
})
local old_buf = api.nvim_get_current_buf()
command('new')
fn.chanclose(api.nvim_open_term(0, {}))
local term_buf = api.nvim_get_current_buf()
screen:expect([[
^ |
[Terminal closed] |
{31:[Scratch] }|
|
{1:~ }|
{2:[No Name] }|
|
]])
-- Autocommand should not result in the wrong buffer being wiped out.
command('autocmd TermLeave * ++once wincmd p')
feed('ii')
screen:expect([[
^ |
{1:~ }|*5
|
]])
eq(old_buf, api.nvim_get_current_buf())
eq(false, api.nvim_buf_is_valid(term_buf))
term_buf = api.nvim_get_current_buf()
fn.chanclose(api.nvim_open_term(term_buf, {}))
screen:expect([[
^ |
[Terminal closed] |
|*5
]])
-- Autocommand should not result in a heap UAF if it frees the terminal prematurely.
command('autocmd TermLeave * ++once bwipeout!')
feed('ii')
screen:expect([[
^ |
{1:~ }|*5
|
]])
eq(false, api.nvim_buf_is_valid(term_buf))
end)
end)
describe('on_lines does not emit out-of-bounds line indexes when', function()

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
@@ -470,6 +473,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()

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 |
{8: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([[
{8: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()
^ |
{3:-- 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 |
^ |
{3:-- 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([[
{8:line26} |
line27 |
line28 |
line29 |
line30 |
^ |
{3:-- 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()
@@ -658,48 +785,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 |
^ |
{3:-- 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([[
{8:line26} |
line27 |
line28 |
line29 |
line30 |
^ |
{3:-- 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')

View File

@@ -1,10 +1,13 @@
local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
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
@@ -189,6 +192,427 @@ describe(':terminal window', function()
]])
end)
end)
it('redrawn when restoring cursorline/column', function()
screen:set_default_attr_ids({
[1] = { bold = true },
[2] = { foreground = 130 },
[3] = { foreground = 130, underline = true },
[12] = { underline = true },
[19] = { background = 7 },
})
feed([[<C-\><C-N>]])
command('setlocal cursorline')
screen:expect([[
tty ready |
{12:^ }|
|*5
]])
feed('i')
screen:expect([[
tty ready |
^ |
|*4
{1:-- TERMINAL --} |
]])
feed([[<C-\><C-N>]])
screen:expect([[
tty ready |
{12:^ }|
|*5
]])
command('setlocal number')
screen:expect([[
{2: 1 }tty ready |
{3: 2 }{12:^rows: 6, cols: 46 }|
{2: 3 } |
{2: 4 } |
{2: 5 } |
{2: 6 } |
|
]])
feed('i')
screen:expect([[
{2: 1 }tty ready |
{2: 2 }rows: 6, cols: 46 |
{3: 3 }^ |
{2: 4 } |
{2: 5 } |
{2: 6 } |
{1:-- TERMINAL --} |
]])
feed([[<C-\><C-N>]])
screen:expect([[
{2: 1 }tty ready |
{2: 2 }rows: 6, cols: 46 |
{3: 3 }{12:^ }|
{2: 4 } |
{2: 5 } |
{2: 6 } |
|
]])
command('setlocal nonumber nocursorline cursorcolumn')
screen:expect([[
{19:t}ty ready |
{19:r}ows: 6, cols: 46 |
^rows: 6, cols: 50 |
{19: } |*3
|
]])
feed('i')
screen:expect([[
tty ready |
rows: 6, cols: 46 |
rows: 6, cols: 50 |
^ |
|*2
{1:-- TERMINAL --} |
]])
feed([[<C-\><C-N>]])
screen:expect([[
{19:t}ty ready |
{19:r}ows: 6, cols: 46 |
{19:r}ows: 6, cols: 50 |
^ |
{19: } |*2
|
]])
end)
it('redraws cursor info in terminal mode', function()
skip(is_os('win'), '#31587')
command('file AMOGUS | set laststatus=2 ruler')
screen:expect([[
tty ready |
rows: 5, cols: 50 |
^ |
|*2
{17:AMOGUS 3,0-1 All}|
{3:-- TERMINAL --} |
]])
feed_data('you are the imposter')
screen:expect([[
tty ready |
rows: 5, cols: 50 |
you are the imposter^ |
|*2
{17:AMOGUS 3,21 All}|
{3:-- TERMINAL --} |
]])
feed([[<C-\><C-N>]])
screen:expect([[
tty ready |
rows: 5, cols: 50 |
you are the imposte^r |
|*2
{17:AMOGUS 3,20 All}|
|
]])
end)
it('redraws stale statuslines and mode when not updating screen', function()
command('file foo | set ruler | vsplit')
screen:expect([[
tty ready │tty ready |
rows: 5, cols: 25 │rows: 5, cols: 25 |
^ │ |
│ |*2
{17:foo 3,0-1 All }{18:foo 2,0-1 Top}|
{3:-- TERMINAL --} |
]])
command("call win_execute(win_getid(winnr('#')), 'call cursor(1, 1)')")
screen:expect([[
tty ready │tty ready |
rows: 5, cols: 25 │rows: 5, cols: 25 |
^ │ |
│ |*2
{17:foo 3,0-1 All }{18:foo 1,1 All}|
{3:-- TERMINAL --} |
]])
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 |
{3:-- 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 |
{3:-- 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('in new tabpage has correct terminal size', function()
screen:set_default_attr_ids({
[1] = { reverse = true },
[3] = { bold = true },
[17] = { background = 2, foreground = Screen.colors.Grey0 },
[18] = { background = 2, foreground = 8 },
[19] = { underline = true, foreground = Screen.colors.Grey0, background = 7 },
[20] = { underline = true, foreground = 5, background = 7 },
})
command('file foo | vsplit')
screen:expect([[
tty ready │tty ready |
rows: 5, cols: 25 │rows: 5, cols: 25 |
^ │ |
│ |*2
{17:foo }{18:foo }|
{3:-- TERMINAL --} |
]])
command('tab split')
screen:expect([[
{19: }{20:2}{19: foo }{3: foo }{1: }{19:X}|
tty ready |
rows: 5, cols: 25 |
rows: 5, cols: 50 |
^ |
|
{3:-- TERMINAL --} |
]])
end)
it('restores window options when switching terminals', function()
-- Make this a screen test to also check for proper redrawing.
screen:set_default_attr_ids({
[1] = { bold = true },
[2] = { foreground = Screen.colors.Gray0, background = 7, underline = true },
[3] = { foreground = 5, background = 7, underline = true },
[4] = { reverse = true },
[5] = { bold = true, foreground = 5 },
[6] = { foreground = 12 },
[7] = { reverse = true, bold = true },
[12] = { underline = true },
[17] = { foreground = Screen.colors.Gray0, background = 2 },
[18] = { foreground = 8, background = 2 },
[19] = { background = 7 },
})
feed([[<C-\><C-N>]])
command([[
file foo
setlocal cursorline
vsplit
setlocal nocursorline cursorcolumn
]])
screen:expect([[
{19:t}ty ready │tty ready |
^rows: 5, cols: 25 │{12:rows: 5, cols: 25 }|
{19: } │ |*3
{17:foo }{18:foo }|
|
]])
feed('i')
screen:expect([[
tty ready │tty ready |
rows: 5, cols: 25 │{12:rows: 5, cols: 25 }|
^ │ |
│ |*2
{17:foo }{18:foo }|
{1:-- TERMINAL --} |
]])
command('wincmd p')
screen:expect([[
{19:t}ty ready │tty ready |
{19:r}ows: 5, cols: 25 │rows: 5, cols: 25 |
│^ |
{19: } │ |*2
{18:foo }{17:foo }|
{1:-- TERMINAL --} |
]])
feed([[<C-\><C-N>]])
screen:expect([[
{19:t}ty ready │tty ready |
{19:r}ows: 5, cols: 25 │rows: 5, cols: 25 |
│{12:^ }|
{19: } │ |*2
{18:foo }{17:foo }|
|
]])
-- Ensure things work when switching tabpages.
command('tab split | setlocal cursorline cursorcolumn')
screen:expect([[
{2: }{3:2}{2: foo }{1: foo }{4: }{2:X}|
{19:t}ty ready |
{19:r}ows: 5, cols: 25 |
{12:^rows: 5, cols: 50 }|
{19: } |*2
|
]])
feed('i')
screen:expect([[
{2: }{3:2}{2: foo }{1: foo }{4: }{2:X}|
tty ready |
rows: 5, cols: 25 |
rows: 5, cols: 50 |
^ |
|
{1:-- TERMINAL --} |
]])
command('tabprevious')
screen:expect([[
{1: }{5:2}{1: foo }{2: foo }{4: }{2:X}|
{19:r}ows: 5, cols: 25 │rows: 5, cols: 25 |
rows: 5, cols: 50 │rows: 5, cols: 50 |
{19: } │^ |
{19: } │ |
{18:foo }{17:foo }|
{1:-- TERMINAL --} |
]])
feed([[<C-\><C-N>]])
screen:expect([[
{1: }{5:2}{1: foo }{2: foo }{4: }{2:X}|
{19:r}ows: 5, cols: 25 │rows: 5, cols: 25 |
rows: 5, cols: 50 │rows: 5, cols: 50 |
{19: } │{12:^ }|
{19: } │ |
{18:foo }{17:foo }|
|
]])
command('tabnext')
screen:expect([[
{2: }{3:2}{2: foo }{1: foo }{4: }{2:X}|
{19:t}ty ready |
{19:r}ows: 5, cols: 25 |
{19:r}ows: 5, cols: 50 |
{12:^ }|
{19: } |
|
]])
-- Closing windows shouldn't break things.
command('tabprevious')
feed('i')
screen:expect([[
{1: }{5:2}{1: foo }{2: foo }{4: }{2:X}|
{19:r}ows: 5, cols: 25 │rows: 5, cols: 25 |
rows: 5, cols: 50 │rows: 5, cols: 50 |
{19: } │^ |
{19: } │ |
{18:foo }{17:foo }|
{1:-- TERMINAL --} |
]])
command('quit')
screen:expect([[
{1: foo }{2: foo }{4: }{2:X}|
tty ready |
rows: 5, cols: 25 |
rows: 5, cols: 50 |
^ |
|
{1:-- TERMINAL --} |
]])
feed([[<C-\><C-N>]])
screen:expect([[
{1: foo }{2: foo }{4: }{2:X}|
{19:t}ty ready |
{19:r}ows: 5, cols: 25 |
{19:r}ows: 5, cols: 50 |
^ |
{19: } |
|
]])
-- Switching to a non-terminal.
command('vnew')
feed([[<C-W>pi]])
screen:expect([[
{1: }{5:2}{1: foo }{2: foo }{4: }{2:X}|
│rows: 5, cols: 25 |
{6:~ }│rows: 5, cols: 50 |
{6:~ }│^ |
{6:~ }│ |
{4:[No Name] }{17:foo }|
{1:-- TERMINAL --} |
]])
command('wincmd p')
screen:expect([[
{1: }{5:2}{1: [No Name] }{2: foo }{4: }{2:X}|
^ │{19:r}ows: 5, cols: 25 |
{6:~ }│{19:r}ows: 5, cols: 50 |
{6:~ }│ |
{6:~ }│{19: } |
{7:[No Name] }{18:foo }|
|
]])
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()

View File

@@ -79,18 +79,27 @@ describe('title', function()
it('is updated in Terminal mode', function()
api.nvim_set_option_value('title', true, {})
api.nvim_set_option_value('titlestring', '(%{mode(1)}) | nvim', {})
api.nvim_set_option_value('titlestring', '%t (%{mode(1)}) | nvim', {})
fn.jobstart({ n.testprg('shell-test'), 'INTERACT' }, { term = true })
api.nvim_buf_set_name(0, 'shell-test')
screen:expect(function()
eq('(nt) | nvim', screen.title)
eq('shell-test (nt) | nvim', screen.title)
end)
feed('i')
screen:expect(function()
eq('(t) | nvim', screen.title)
eq('shell-test (t) | nvim', screen.title)
end)
api.nvim_set_option_value('titlelen', 1, {})
screen:expect(function()
eq('<t) | nvim', screen.title)
end)
command('set titlelen&')
screen:expect(function()
eq('shell-test (t) | nvim', screen.title)
end)
feed([[<C-\><C-N>]])
screen:expect(function()
eq('(nt) | nvim', screen.title)
eq('shell-test (nt) | nvim', screen.title)
end)
end)

View File

@@ -619,17 +619,48 @@ func Test_switch_to_previously_viewed_buffer()
vsplit
call cursor(100, 3)
call assert_equal('100', getline('.'))
edit Xotherbuf
buffer Xviewbuf
call assert_equal([0, 100, 3, 0], getpos('.'))
call assert_equal('100', getline('.'))
edit Xotherbuf
wincmd p
normal! gg10dd
wincmd p
buffer Xviewbuf
call assert_equal([0, 90, 3, 0], getpos('.'))
call assert_equal('100', getline('.'))
edit Xotherbuf
wincmd p
normal! ggP
wincmd p
buffer Xviewbuf
call assert_equal([0, 100, 3, 0], getpos('.'))
call assert_equal('100', getline('.'))
edit Xotherbuf
wincmd p
normal! 96gg10ddgg
wincmd p
buffer Xviewbuf
" The original cursor line was deleted, so cursor is restored to the start
" of the line before the deleted range.
call assert_equal([0, 95, 1, 0], getpos('.'))
call assert_equal('95', getline('.'))
normal! u
exe win_id2win(oldwin) .. 'close'
setlocal bufhidden=hide
call cursor(200, 3)
call assert_equal('200', getline('.'))
edit Xotherbuf
buffer Xviewbuf
call assert_equal([0, 200, 3, 0], getpos('.'))
call assert_equal('200', getline('.'))
bwipe! Xotherbuf
bwipe! Xviewbuf