commit b7124ae7684d4f6053c74c914a8faf8d98e5d5b3
parent d07cbb2f4225238c345d2f858c9d3e8060fbc995
Author: Sean Dewar <6256228+seandewar@users.noreply.github.com>
Date: Mon, 3 Mar 2025 15:53:03 +0000
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.
Diffstat:
3 files changed, 323 insertions(+), 46 deletions(-)
diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c
@@ -111,6 +111,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;
#include "terminal.c.generated.h"
@@ -666,6 +676,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)
{
@@ -687,33 +766,7 @@ 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;
- }
- 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);
- }
+ set_terminal_winopts(s);
s->term->pending.cursor = true; // Update the cursor shape table
adjust_topline(s->term, buf, 0); // scroll to end
@@ -744,25 +797,7 @@ bool terminal_enter(void)
// 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.
- 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);
- }
+ unset_terminal_winopts(s);
// Tell the terminal it lost focus
terminal_focus(s->term, false);
@@ -949,11 +984,19 @@ static int terminal_execute(VimState *state, int key)
if (curbuf->terminal == NULL) {
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) {
// 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 1;
}
diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua
@@ -337,6 +337,73 @@ 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.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
+ {120:foo [-] }{119:foo [-] bar [-] }|
+ |
+ ]])
+
+ feed('i')
+ screen:expect([[
+ focused │focused │ |
+ ^ │ │ |
+ │ │ |*3
+ {120:foo [-] }{119:foo [-] bar [-] }|
+ {5:-- TERMINAL --} |
+ ]])
+ -- Next window has the same terminal; no new notifications.
+ command('wincmd w')
+ screen:expect([[
+ focused │focused │ |
+ │^ │ |
+ │ │ |*3
+ {119:foo [-] }{120:foo [-] }{119:bar [-] }|
+ {5:-- TERMINAL --} |
+ ]])
+ -- Next window has a different terminal; expect new unfocus and focus notifications.
+ command('wincmd w')
+ screen:expect([[
+ focused │focused │focused |
+ unfocused │unfocuse│^ |
+ │ │ |*3
+ {119:foo [-] foo [-] }{120:bar [-] }|
+ {5:-- TERMINAL --} |
+ ]])
+ -- Leaving terminal mode; expect a new unfocus notification.
+ feed([[<C-\><C-N>]])
+ screen:expect([[
+ focused │focused │focused |
+ unfocused │unfocuse│unfocused |
+ │ │^ |
+ │ │ |*2
+ {119:foo [-] foo [-] }{120:bar [-] }|
+ |
+ ]])
+ end)
end)
describe(':terminal buffer', function()
diff --git a/test/functional/terminal/window_spec.lua b/test/functional/terminal/window_spec.lua
@@ -415,6 +415,173 @@ describe(':terminal window', function()
]])
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()