fix(ui2): :$q should not target hidden windows #40992

Similar to #36123, the ui2 windows confuse `:$q`, which doesn't close
the last window on the tab, but targets a hidden ui2 window.

The problem seems to be a combination of using
`FOR_ALL_WINDOWS_IN_TAB()`, which includes hidden windows, and then
counting the windows without factoring in `win_has_winnr()`.

There are examples of using `win_has_winnr()`, such as:
- [`eval/buffer.c`'s `buf_win_common()`](4a5062cda6/src/nvim/eval/buffer.c (L474-L475))
- [`eval/window.c`'s `f_getwininfo()`](4a5062cda6/src/nvim/eval/window.c (L130-L135))
- [`window.c`'s `win_get_tabwin()`](4a5062cda6/src/nvim/window.c (L7860-L7869))

But possibly problematic ones:
- `ex_docmd.c`'s:
	- [`invalid_range()`](4a5062cda6/src/nvim/ex_docmd.c (L3891-L3893))
		- ^ should this include hidden windows? Can a user address them by number? `win_has_winnr()` suggests not
	- [`ex_close()`](4a5062cda6/src/nvim/ex_docmd.c (L5233-L5244))
	- [`ex_hide()`](4a5062cda6/src/nvim/ex_docmd.c (L5478-L5484))
- `eval/window.c`'s:
	- [`f_winrestcmd()`](4a5062cda6/src/nvim/eval/window.c (L797-L808))
	- [`find_win_by_nr()`](4a5062cda6/src/nvim/eval/window.c (L172-L180))
This commit is contained in:
Rob Pilling
2026-08-02 19:37:26 +01:00
committed by GitHub
parent ecae8c994e
commit 2f09090134
3 changed files with 55 additions and 34 deletions

View File

@@ -48,6 +48,20 @@ bool win_has_winnr(win_T *wp, tabpage_T *tp)
|| (!wp->w_config.hide && wp->w_config.focusable);
}
win_T *win_find_nr(int nr, tabpage_T *tp)
{
if (tp == NULL) {
assert(curtab);
tp = curtab;
}
FOR_ALL_WINDOWS_IN_TAB(wp, tp) {
if ((nr -= win_has_winnr(wp, tp)) <= 0) {
return wp;
}
}
return NULL;
}
static int win_getid(typval_T *argvars)
{
if (argvars[0].v_type == VAR_UNKNOWN) {
@@ -169,16 +183,17 @@ win_T *find_win_by_nr(typval_T *vp, tabpage_T *tp)
tp = curtab;
}
FOR_ALL_WINDOWS_IN_TAB(wp, tp) {
if (nr >= LOWEST_WIN_ID) {
if (nr >= LOWEST_WIN_ID) {
// window id
FOR_ALL_WINDOWS_IN_TAB(wp, tp) {
if (wp->handle == nr) {
return wp;
}
} else if (--nr <= 0) {
return wp;
}
return NULL;
}
return NULL;
return win_find_nr(nr, tp);
}
/// Find a window: When using a Window ID in any tab page, when using a number

View File

@@ -44,6 +44,7 @@
#include "nvim/eval/typval_defs.h"
#include "nvim/eval/userfunc.h"
#include "nvim/eval/vars.h"
#include "nvim/eval/window.h"
#include "nvim/eval_defs.h"
#include "nvim/event/loop.h"
#include "nvim/event/multiqueue.h"
@@ -1075,14 +1076,24 @@ static int compute_buffer_local_count(cmd_addr_T addr_type, linenr_T lnum, int o
/// @return the window number of "win" or,
/// the number of windows if "win" is NULL
static int current_win_nr(const win_T *win)
static int current_win_nr(const win_T *win, exarg_T *cmdarg)
FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
// Whether to also match hidden / non-focusable windows (in current tab).
bool nonnr = cmdarg != NULL && cmdarg->cmdidx != CMD_quit;
int nr = 0;
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
nr++;
// only include non-hidden and focusable windows
// (so ui2 windows are excluded)
bool include_this = nonnr || win_has_winnr(wp, curtab);
nr += include_this;
if (wp == win) {
// same as in win_id2win()
if (!include_this) {
return 0;
}
break;
}
}
@@ -1102,8 +1113,8 @@ static int current_tab_nr(tabpage_T *tab)
return nr;
}
#define CURRENT_WIN_NR current_win_nr(curwin)
#define LAST_WIN_NR current_win_nr(NULL)
#define CURRENT_WIN_NR current_win_nr(curwin, NULL)
#define LAST_WIN_NR(cmdarg) current_win_nr(NULL, (cmdarg))
#define CURRENT_TAB_NR current_tab_nr(curtab)
#define LAST_TAB_NR current_tab_nr(NULL)
@@ -1304,7 +1315,7 @@ void set_cmd_dflall_range(exarg_T *eap)
eap->line2 = lastbuf->b_fnum;
break;
case ADDR_WINDOWS:
eap->line2 = LAST_WIN_NR;
eap->line2 = LAST_WIN_NR(eap);
break;
case ADDR_TABS:
eap->line2 = LAST_TAB_NR;
@@ -2854,7 +2865,7 @@ int parse_cmd_address(exarg_T *eap, const char **errormsg, bool silent)
if (IS_USER_CMDIDX(eap->cmdidx)) {
eap->line1 = 1;
eap->line2 = eap->addr_type == ADDR_WINDOWS
? LAST_WIN_NR : LAST_TAB_NR;
? LAST_WIN_NR(false) : LAST_TAB_NR;
} else {
// there is no Vim command which uses '%' and
// ADDR_WINDOWS or ADDR_TABS
@@ -3429,7 +3440,7 @@ linenr_T get_address(exarg_T *eap, char **ptr, cmd_addr_T addr_type, bool skip,
lnum = curbuf->b_ml.ml_line_count;
break;
case ADDR_WINDOWS:
lnum = LAST_WIN_NR;
lnum = LAST_WIN_NR(eap);
break;
case ADDR_ARGUMENTS:
lnum = ARGCOUNT;
@@ -3773,7 +3784,7 @@ char *invalid_range(exarg_T *eap)
}
break;
case ADDR_WINDOWS:
if (eap->line2 > LAST_WIN_NR) {
if (eap->line2 > LAST_WIN_NR(eap)) {
return _(e_invrange);
}
break;
@@ -5124,24 +5135,17 @@ fail_1:
/// ":close": close current window, unless it is the last one
static void ex_close(exarg_T *eap)
{
win_T *win = NULL;
int winnr = 0;
if (!text_locked() && !curbuf_locked()) {
win_T *win;
if (eap->addr_count == 0) {
ex_win_close(eap->forceit, curwin, NULL);
win = curwin;
} else {
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
winnr++;
if (winnr == eap->line2) {
win = wp;
break;
}
}
win = win_find_nr((int)eap->line2, curtab);
if (win == NULL) {
win = lastwin;
}
ex_win_close(eap->forceit, win, NULL);
}
ex_win_close(eap->forceit, win, NULL);
}
}
@@ -5369,19 +5373,11 @@ static void ex_hide(exarg_T *eap)
return;
}
win_T *win = NULL;
win_T *win;
if (eap->addr_count == 0) {
win = curwin;
} else {
int winnr = 0;
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
winnr++;
if (winnr == eap->line2) {
win = wp;
break;
}
}
win = win_find_nr((int)eap->line2, curtab);
if (win == NULL) {
win = lastwin;
}

View File

@@ -324,6 +324,16 @@ describe('cmdline2', function()
{16::}{15:call} {25:foo}{16:(}{25:bar}{16:(}^ |
]])
end)
it("doesn't interfere with :$q", function()
exec('split b')
exec('wincmd J')
exec('wincmd p')
exec('$q') -- this would close a ui2 window, not the second visible window
local nwins = vim.fn.winnr('$')
assert(nwins == 1)
end)
end)
describe('cmdline2', function()