fix(terminal): update winopts and focus when switching terminals

Problem: window options and terminal focus notifications not updated when
switching terminals without leaving terminal mode.

Solution: update them.
This commit is contained in:
Sean Dewar
2025-03-03 15:53:03 +00:00
committed by zeertzjq
parent 934d28558d
commit 2eea65fe68
3 changed files with 323 additions and 46 deletions

View File

@@ -112,6 +112,16 @@ typedef struct {
bool got_bsl; ///< if the last input was <C-\> bool got_bsl; ///< if the last input was <C-\>
bool got_bsl_o; ///< if left terminal mode with <c-\><c-o> 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 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; } TerminalState;
#ifdef INCLUDE_GENERATED_DECLARATIONS #ifdef INCLUDE_GENERATED_DECLARATIONS
@@ -671,6 +681,75 @@ void terminal_check_size(Terminal *term)
invalidate_terminal(term, -1, -1); 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 /// Implements MODE_TERMINAL state. :help Terminal-mode
bool terminal_enter(void) bool terminal_enter(void)
{ {
@@ -692,33 +771,7 @@ bool terminal_enter(void)
mapped_ctrl_c |= MODE_TERMINAL; // Always map CTRL-C to avoid interrupt. mapped_ctrl_c |= MODE_TERMINAL; // Always map CTRL-C to avoid interrupt.
RedrawingDisabled = false; RedrawingDisabled = false;
// Disable these options in terminal-mode. They are nonsense because cursor is set_terminal_winopts(s);
// 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;
}
curwin->w_p_cuc = false;
curwin->w_p_so = 0;
curwin->w_p_siso = 0;
if (curwin->w_p_cuc != save_w_p_cuc) {
redraw_later(curwin, UPD_SOME_VALID);
} else if (curwin->w_p_cul != save_w_p_cul
|| (curwin->w_p_cul && curwin->w_p_culopt_flags != save_w_p_culopt_flags)) {
redraw_later(curwin, UPD_VALID);
}
s->term->pending.cursor = true; // Update the cursor shape table s->term->pending.cursor = true; // Update the cursor shape table
adjust_topline(s->term, buf, 0); // scroll to end adjust_topline(s->term, buf, 0); // scroll to end
@@ -749,25 +802,7 @@ bool terminal_enter(void)
// Restore the terminal cursor to what is set in 'guicursor' // Restore the terminal cursor to what is set in 'guicursor'
(void)parse_shape_opt(SHAPE_CURSOR); (void)parse_shape_opt(SHAPE_CURSOR);
if (save_curwin == curwin->handle) { // Else: window was closed. unset_terminal_winopts(s);
if (save_w_p_cuc != curwin->w_p_cuc) {
redraw_later(curwin, UPD_SOME_VALID);
} else if (save_w_p_cul != curwin->w_p_cul
|| (save_w_p_cul && save_w_p_culopt_flags != curwin->w_p_culopt_flags)) {
redraw_later(curwin, UPD_VALID);
}
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);
}
// Tell the terminal it lost focus // Tell the terminal it lost focus
terminal_focus(s->term, false); terminal_focus(s->term, false);
@@ -954,11 +989,19 @@ static int terminal_execute(VimState *state, int key)
if (curbuf->terminal == NULL) { if (curbuf->terminal == NULL) {
return 0; return 0;
} }
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) { if (s->term != curbuf->terminal) {
// Active terminal buffer changed, flush terminal's cursor state to the UI // Active terminal buffer changed, flush terminal's cursor state to the UI
terminal_focus(s->term, false);
s->term = curbuf->terminal; s->term = curbuf->terminal;
s->term->pending.cursor = true; s->term->pending.cursor = true;
invalidate_terminal(s->term, -1, -1); invalidate_terminal(s->term, -1, -1);
terminal_focus(s->term, true);
} }
return 1; return 1;
} }

View File

@@ -343,6 +343,73 @@ describe(':terminal buffer', function()
|*4 |*4
]]) ]])
end) 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.api.nvim_chan_send(term, 'focused\n')
elseif data == '\27[O' then
vim.api.nvim_chan_send(term, 'unfocused\n')
end
end,
})
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 }|
|
]])
feed('i')
screen:expect([[
focused │focused │ |
^ │ │ |
│ │ |*3
{17:foo }{18:foo bar }|
{3:-- TERMINAL --} |
]])
-- 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 }|
|
]])
end)
end) end)
describe(':terminal buffer', function() describe(':terminal buffer', function()

View File

@@ -415,6 +415,173 @@ describe(':terminal window', function()
]]) ]])
end) 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() it('not unnecessarily redrawn by events', function()
eq('t', eval('mode()')) eq('t', eval('mode()'))
exec_lua(function() exec_lua(function()