summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorluukvbaal <luukvbaal@gmail.com>2026-04-20 02:36:55 +0200
committerGitHub <noreply@github.com>2026-04-19 20:36:55 -0400
commitfe986e5dd094b2f7e1d28e64e52ffbc5f7292191 (patch)
treeeae04471b5d12c3e419b4961c8f5405706ffb47b
parent5f6abd34f5c12534df0d79ee3507d40106ad505d (diff)
feat(options): add 'winpinned' to pin a window #39157
Problem: - Unable to "pin" a window to prevent closing without specifically being targeted. - :fclose closes hidden windows (even before visible windows). Solution: - Add 'winpinned' window-local option. When set, window is skipped by :fclose and :only. Pin the ui2 cmdline window (which should always be visible), so that it is not closed by :only/fclose. - Skip over hidden (and pinned) windows with :fclose. Co-authored-by: glepnir <glephunter@gmail.com>
-rw-r--r--runtime/doc/news.txt2
-rw-r--r--runtime/doc/options.txt8
-rw-r--r--runtime/lua/vim/_core/ui2.lua2
-rw-r--r--runtime/lua/vim/_meta/options.gen.lua10
-rw-r--r--runtime/scripts/optwin.lua1
-rwxr-xr-xsrc/gen/gen_eval_files.lua1
-rw-r--r--src/nvim/buffer_defs.h2
-rw-r--r--src/nvim/ex_docmd.c4
-rw-r--r--src/nvim/option.c2
-rw-r--r--src/nvim/options.lua13
-rw-r--r--src/nvim/window.c23
-rw-r--r--src/nvim/winfloat.c3
-rw-r--r--test/functional/api/window_spec.lua24
-rw-r--r--test/functional/ui/messages2_spec.lua2
14 files changed, 88 insertions, 9 deletions
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index ccedbee310..f287b68d29 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -156,7 +156,7 @@ the same range instances now compare equal.
OPTIONS
-• todo
+• 'winpinned' prevents window from closing unless specifically targeted.
PERFORMANCE
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index f2bca82cc8..de92fc98ff 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -296,6 +296,7 @@ created, thus they behave slightly differently:
'winfixbuf' specific to existing window
'winfixheight' specific to existing window
'winfixwidth' specific to existing window
+ 'winpinned' specific to existing window
Special local buffer options
@@ -7626,6 +7627,13 @@ A jump table for the options with a short description can be found at |Q_op|.
large number, it will cause errors when opening more than a few
windows. A value of 0 to 12 is reasonable.
+ *'winpinned'* *'wp'* *'nowinpinned'* *'nowp'*
+'winpinned' 'wp' boolean (default off)
+ local to window |local-noglobal|
+ If enabled, the window is pinned and will not be closed by |:only|
+ and |:fclose|. Only commands specifically targeting the window can
+ close it.
+
*'winwidth'* *'wiw'* *E592*
'winwidth' 'wiw' number (default 20)
global
diff --git a/runtime/lua/vim/_core/ui2.lua b/runtime/lua/vim/_core/ui2.lua
index 77ea7f57b3..4c7ceefda0 100644
--- a/runtime/lua/vim/_core/ui2.lua
+++ b/runtime/lua/vim/_core/ui2.lua
@@ -130,6 +130,8 @@ function M.check_targets()
hl = 'Normal:MsgArea'
elseif type == 'msg' then
hl = search_hide
+ elseif type == 'cmd' then
+ api.nvim_set_option_value('winpinned', true, { scope = 'local' })
end
api.nvim_set_option_value('winhighlight', hl, { scope = 'local' })
end)
diff --git a/runtime/lua/vim/_meta/options.gen.lua b/runtime/lua/vim/_meta/options.gen.lua
index c151c448bb..82f53eedc7 100644
--- a/runtime/lua/vim/_meta/options.gen.lua
+++ b/runtime/lua/vim/_meta/options.gen.lua
@@ -8324,6 +8324,16 @@ vim.o.wmw = vim.o.winminwidth
vim.go.winminwidth = vim.o.winminwidth
vim.go.wmw = vim.go.winminwidth
+--- If enabled, the window is pinned and will not be closed by `:only`
+--- and `:fclose`. Only commands specifically targeting the window can
+--- close it.
+---
+--- @type boolean
+vim.o.winpinned = false
+vim.o.wp = vim.o.winpinned
+vim.wo.winpinned = vim.o.winpinned
+vim.wo.wp = vim.wo.winpinned
+
--- Minimal number of columns for the current window. This is not a hard
--- minimum, Vim will use fewer columns if there is not enough room. If
--- the current window is smaller, its size is increased, at the cost of
diff --git a/runtime/scripts/optwin.lua b/runtime/scripts/optwin.lua
index f91cbedc67..6961182cf3 100644
--- a/runtime/scripts/optwin.lua
+++ b/runtime/scripts/optwin.lua
@@ -128,6 +128,7 @@ local options_list = {
{ 'winfixwidth', N_ 'keep the width of the window' },
{ 'winwidth', N_ 'minimal number of columns used for the current window' },
{ 'winminwidth', N_ 'minimal number of columns used for any window' },
+ { 'winpinned', N_ 'prevent closing window with :only and :fclose' },
{ 'helpheight', N_ 'initial height of the help window' },
{ 'previewheight', N_ 'default height for the preview window' },
{ 'previewwindow', N_ 'identifies the preview window' },
diff --git a/src/gen/gen_eval_files.lua b/src/gen/gen_eval_files.lua
index 4609509042..05f2d52fcf 100755
--- a/src/gen/gen_eval_files.lua
+++ b/src/gen/gen_eval_files.lua
@@ -667,6 +667,7 @@ local function option_scope_doc(o)
'syntax',
'winfixheight',
'winfixwidth',
+ 'winpinned',
}, o.full_name)
then
r = r .. ' |local-noglobal|'
diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h
index 6b1719cc6e..546d6e5a6a 100644
--- a/src/nvim/buffer_defs.h
+++ b/src/nvim/buffer_defs.h
@@ -148,6 +148,8 @@ typedef struct {
#define w_p_wfh w_onebuf_opt.wo_wfh // 'winfixheight'
int wo_wfw;
#define w_p_wfw w_onebuf_opt.wo_wfw // 'winfixwidth'
+ int wo_wp;
+#define w_p_wp w_onebuf_opt.wo_wp // 'winpinned'
int wo_pvw;
#define w_p_pvw w_onebuf_opt.wo_pvw // 'previewwindow'
OptInt wo_lhi;
diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c
index 34aef2e476..e527b38b6a 100644
--- a/src/nvim/ex_docmd.c
+++ b/src/nvim/ex_docmd.c
@@ -5329,7 +5329,7 @@ void tabpage_close(int forceit)
ex_win_close(forceit, curwin, NULL);
}
if (!ONE_WINDOW) {
- close_others(true, forceit);
+ close_others(true, forceit, true);
}
if (ONE_WINDOW) {
ex_win_close(forceit, curwin, NULL);
@@ -5402,7 +5402,7 @@ static void ex_only(exarg_T *eap)
win_goto(wp);
}
}
- close_others(true, eap->forceit);
+ close_others(true, eap->forceit, false);
}
static void ex_hide(exarg_T *eap)
diff --git a/src/nvim/option.c b/src/nvim/option.c
index 030702ccaf..51403a0fe6 100644
--- a/src/nvim/option.c
+++ b/src/nvim/option.c
@@ -4859,6 +4859,8 @@ void *get_varp_from(vimoption_T *p, buf_T *buf, win_T *win)
return &(win->w_p_wfh);
case kOptWinfixwidth:
return &(win->w_p_wfw);
+ case kOptWinpinned:
+ return &(win->w_p_wp);
case kOptPreviewwindow:
return &(win->w_p_pvw);
case kOptLhistory:
diff --git a/src/nvim/options.lua b/src/nvim/options.lua
index 5ec0fe842a..a24da96883 100644
--- a/src/nvim/options.lua
+++ b/src/nvim/options.lua
@@ -10714,6 +10714,19 @@ local options = {
varname = 'p_wmw',
},
{
+ abbreviation = 'wp',
+ defaults = false,
+ desc = [=[
+ If enabled, the window is pinned and will not be closed by |:only|
+ and |:fclose|. Only commands specifically targeting the window can
+ close it.
+ ]=],
+ full_name = 'winpinned',
+ scope = { 'win' },
+ short_desc = N_('prevent closing window with :only and :fclose'),
+ type = 'boolean',
+ },
+ {
abbreviation = 'wiw',
cb = 'did_set_winwidth',
defaults = 20,
diff --git a/src/nvim/window.c b/src/nvim/window.c
index b7d3bce5c5..a82dfa0f80 100644
--- a/src/nvim/window.c
+++ b/src/nvim/window.c
@@ -4248,10 +4248,12 @@ static int frame_minwidth(frame_T *topfrp, win_T *next_curwin)
/// Buffers in the other windows become hidden if 'hidden' is set, or '!' is
/// used and the buffer was modified.
///
-/// Used by ":bdel" and ":only".
+/// Used by ":tabclose" and ":only".
///
-/// @param forceit always hide all other windows
-void close_others(int message, int forceit)
+/// @param message if true, display error messages
+/// @param forceit always hide all other windows
+/// @param ignore_pinned if true, also close pinned windows (for :tabclose)
+void close_others(int message, int forceit, bool ignore_pinned)
{
win_T *const old_curwin = curwin;
@@ -4280,7 +4282,8 @@ void close_others(int message, int forceit)
curbuf = curwin->w_buffer;
}
- if (wp == curwin) { // don't close current window
+ // don't close current window or pinned windows
+ if (wp == curwin || (wp->w_p_wp && !ignore_pinned)) {
continue;
}
@@ -4312,7 +4315,17 @@ void close_others(int message, int forceit)
}
if (message && !ONE_WINDOW) {
- emsg(_("E445: Other window contains changes"));
+ // Check if remaining windows are non-pinned
+ bool has_non_pinned = false;
+ for (win_T *wp = firstwin; wp != NULL; wp = wp->w_next) {
+ if (wp != curwin && !wp->w_p_wp) {
+ has_non_pinned = true;
+ break;
+ }
+ }
+ if (has_non_pinned) {
+ emsg(_("E445: Other window contains changes"));
+ }
}
}
diff --git a/src/nvim/winfloat.c b/src/nvim/winfloat.c
index f222f6b17e..e949aaf141 100644
--- a/src/nvim/winfloat.c
+++ b/src/nvim/winfloat.c
@@ -298,6 +298,9 @@ void win_float_remove(bool bang, int count)
{
kvec_t(win_T *) float_win_arr = KV_INITIAL_VALUE;
for (win_T *wp = lastwin; wp && wp->w_floating; wp = wp->w_prev) {
+ if (wp->w_config.hide || wp->w_p_wp) {
+ continue;
+ }
kv_push(float_win_arr, wp);
}
if (float_win_arr.size > 0) {
diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua
index daf0ae8ce1..650a4bf5e3 100644
--- a/test/functional/api/window_spec.lua
+++ b/test/functional/api/window_spec.lua
@@ -3861,4 +3861,28 @@ describe('API/win', function()
eq(float_win, api.nvim_get_current_win())
end)
end)
+
+ it(':fclose and :only skip hidden and pinned windows #36123', function()
+ local cfg = { relative = 'editor', row = 0, col = 0, width = 1, height = 1 }
+ local win1 = api.nvim_open_win(0, false, cfg)
+ command('fclose')
+ eq(false, api.nvim_win_is_valid(win1))
+ cfg.hide = true
+ win1 = api.nvim_open_win(0, false, cfg)
+ cfg.hide, cfg.focusable = false, false
+ local win2 = api.nvim_open_win(0, false, cfg)
+ command('fclose')
+ eq(true, api.nvim_win_is_valid(win1))
+ eq(false, api.nvim_win_is_valid(win2))
+ api.nvim_win_set_config(win1, { hide = false })
+ api.nvim_set_option_value('winpinned', true, { win = win1, scope = 'local' })
+ win2 = api.nvim_open_win(0, false, { split = 'right' })
+ api.nvim_set_option_value('winpinned', true, { win = win2, scope = 'local' })
+ command('only')
+ eq(true, api.nvim_win_is_valid(win1))
+ eq(true, api.nvim_win_is_valid(win2))
+ local tab2 = api.nvim_open_tabpage(0, false, {})
+ command('tabclose')
+ eq(tab2, api.nvim_get_current_tabpage())
+ end)
end)
diff --git a/test/functional/ui/messages2_spec.lua b/test/functional/ui/messages2_spec.lua
index c3cf060bc9..f030ca8692 100644
--- a/test/functional/ui/messages2_spec.lua
+++ b/test/functional/ui/messages2_spec.lua
@@ -432,7 +432,7 @@ describe('messages2', function()
screen:expect([[
^ |
{1:~ }|*12
- |
+ foo [+1] |
]])
end)