vim-patch:9.1.0147: Cannot keep a buffer focused in a window

Problem:  Cannot keep a buffer focused in a window
          (Amit Levy)
Solution: Add the 'winfixbuf' window-local option
          (Colin Kennedy)

fixes:  vim/vim#6445
closes: vim/vim#13903

2157035637

N/A patch:
vim-patch:58f1e5c0893a
This commit is contained in:
Colin Kennedy 2023-12-25 20:41:09 -08:00 committed by zeertzjq
parent a09ddd7ce5
commit 141182d6c6
27 changed files with 3414 additions and 23 deletions

View File

@ -114,6 +114,13 @@ wiped out a buffer which contains a mark or is referenced in another way.
You cannot have two buffers with exactly the same name. This includes the
path leading to the file.
*E1513* >
Cannot edit buffer. 'winfixbuf' is enabled
If a window has 'winfixbuf' enabled, you cannot change that window's current
buffer. You need to set 'nowinfixbuf' before continuing. You may use [!] to
force the window to switch buffers, if your command supports it.
*E72* >
Close error on swap file

View File

@ -160,6 +160,8 @@ The following new APIs and features were added.
• 'breakindent' performance is significantly improved for wrapped lines.
• Cursor movement, insertion with [count] and |screenpos()| are now faster.
• |'winfixbuf'| keeps a window focused onto a specific buffer
• |vim.iter()| provides a generic iterator interface for tables and Lua
iterators |for-in|.

View File

@ -6271,6 +6271,8 @@ A jump table for the options with a short description can be found at |Q_op|.
"split" when both are present.
uselast If included, jump to the previously used window when
jumping to errors with |quickfix| commands.
If a window has 'winfixbuf' enabled, 'switchbuf' is currently not
applied to the split window.
*'synmaxcol'* *'smc'*
'synmaxcol' 'smc' number (default 3000)
@ -7170,6 +7172,15 @@ A jump table for the options with a short description can be found at |Q_op|.
Note: Do not confuse this with the height of the Vim window, use
'lines' for that.
*'winfixbuf'* *'wfb'* *'nowinfixbuf'* *'nowfb'*
'winfixbuf' 'wfb' boolean (default off)
local to window
If enabled, the buffer and any window that displays it are paired.
For example, attempting to change the buffer with |:edit| will fail.
Other commands which change a window's buffer such as |:cnext| will
also skip any window with 'winfixbuf' enabled. However if a command
has an "!" option, a window can be forced to switch buffers.
*'winfixheight'* *'wfh'* *'nowinfixheight'* *'nowfh'*
'winfixheight' 'wfh' boolean (default off)
local to window |local-noglobal|

View File

@ -939,6 +939,7 @@ Short explanation of each option: *option-list*
'wildoptions' 'wop' specifies how command line completion is done
'winaltkeys' 'wak' when the windows system handles ALT keys
'window' 'wi' nr of lines to scroll for CTRL-F and CTRL-B
'winfixbuf' 'wfb' keep window focused on a single buffer
'winfixheight' 'wfh' keep window height when opening/closing windows
'winfixwidth' 'wfw' keep window width when opening/closing windows
'winheight' 'wh' minimum number of lines for the current window

View File

@ -402,17 +402,22 @@ If the tag is in the current file this will always work. Otherwise the
performed actions depend on whether the current file was changed, whether a !
is added to the command and on the 'autowrite' option:
tag in file autowrite ~
current file changed ! option action ~
---------------------------------------------------------------------------
yes x x x goto tag
no no x x read other file, goto tag
no yes yes x abandon current file, read other file, goto
tag
no yes no on write current file, read other file, goto
tag
no yes no off fail
---------------------------------------------------------------------------
tag in file autowrite ~
current file changed ! winfixbuf option action ~
-----------------------------------------------------------------------------
yes x x no x goto tag
no no x no x read other file, goto tag
no yes yes no x abandon current file,
read other file, goto tag
no yes no no on write current file,
read other file, goto tag
no yes no no off fail
yes x yes x x goto tag
no no no yes x fail
no yes no yes x fail
no yes no yes on fail
no yes no yes off fail
-----------------------------------------------------------------------------
- If the tag is in the current file, the command will always work.
- If the tag is in another file and the current file was not changed, the
@ -428,6 +433,8 @@ current file changed ! option action ~
the changes, use the ":w" command and then use ":tag" without an argument.
This works because the tag is put on the stack anyway. If you want to lose
the changes you can use the ":tag!" command.
- If the tag is in another file and the window includes 'winfixbuf', the
command will fail. If the tag is in the same file then it may succeed.
*tag-security*
Note that Vim forbids some commands, for security reasons. This works like

View File

@ -6746,6 +6746,8 @@ vim.bo.swf = vim.bo.swapfile
--- "split" when both are present.
--- uselast If included, jump to the previously used window when
--- jumping to errors with `quickfix` commands.
--- If a window has 'winfixbuf' enabled, 'switchbuf' is currently not
--- applied to the split window.
---
--- @type string
vim.o.switchbuf = "uselast"
@ -7874,6 +7876,18 @@ vim.o.wi = vim.o.window
vim.go.window = vim.o.window
vim.go.wi = vim.go.window
--- If enabled, the buffer and any window that displays it are paired.
--- For example, attempting to change the buffer with `:edit` will fail.
--- Other commands which change a window's buffer such as `:cnext` will
--- also skip any window with 'winfixbuf' enabled. However if a command
--- has an "!" option, a window can be forced to switch buffers.
---
--- @type boolean
vim.o.winfixbuf = false
vim.o.wfb = vim.o.winfixbuf
vim.wo.winfixbuf = vim.o.winfixbuf
vim.wo.wfb = vim.wo.winfixbuf
--- Keep the window height when windows are opened or closed and
--- 'equalalways' is set. Also for `CTRL-W_=`. Set by default for the
--- `preview-window` and `quickfix-window`.

View File

@ -444,6 +444,7 @@ if has("statusline")
call <SID>AddOption("statusline", gettext("alternate format to be used for a status line"))
call <SID>OptionG("stl", &stl)
endif
call append("$", "\t" .. s:local_to_window)
call <SID>AddOption("equalalways", gettext("make all windows the same size when adding/removing windows"))
call <SID>BinOptionG("ea", &ea)
call <SID>AddOption("eadirection", gettext("in which direction 'equalalways' works: \"ver\", \"hor\" or \"both\""))
@ -452,6 +453,8 @@ call <SID>AddOption("winheight", gettext("minimal number of lines used for the c
call append("$", " \tset wh=" . &wh)
call <SID>AddOption("winminheight", gettext("minimal number of lines used for any window"))
call append("$", " \tset wmh=" . &wmh)
call <SID>AddOption("winfixbuf", gettext("keep window focused on a single buffer"))
call <SID>OptionG("wfb", &wfb)
call <SID>AddOption("winfixheight", gettext("keep the height of the window"))
call append("$", "\t" .. s:local_to_window)
call <SID>BinOptionL("wfh")

View File

@ -876,6 +876,11 @@ void nvim_set_current_buf(Buffer buffer, Error *err)
return;
}
if (curwin->w_p_wfb) {
api_set_error(err, kErrorTypeException, "%s", e_winfixbuf_cannot_go_to_buffer);
return;
}
try_start();
int result = do_buffer(DOBUF_GOTO, DOBUF_FIRST, FORWARD, buf->b_fnum, 0);
if (!try_end(err) && result == FAIL) {

View File

@ -61,6 +61,12 @@ void nvim_win_set_buf(Window window, Buffer buffer, Error *err)
if (!win || !buf) {
return;
}
if (win->w_p_wfb) {
api_set_error(err, kErrorTypeException, "%s", e_winfixbuf_cannot_go_to_buffer);
return;
}
if (win == cmdwin_win || win == cmdwin_old_curwin || buf == cmdwin_buf) {
api_set_error(err, kErrorTypeException, "%s", e_cmdwin);
return;

View File

@ -623,6 +623,8 @@ void ex_argument(exarg_T *eap)
/// Edit file "argn" of the argument lists.
void do_argfile(exarg_T *eap, int argn)
{
bool is_split_cmd = *eap->cmd == 's';
int old_arg_idx = curwin->w_arg_idx;
if (argn < 0 || argn >= ARGCOUNT) {
@ -637,10 +639,16 @@ void do_argfile(exarg_T *eap, int argn)
return;
}
if (!is_split_cmd
&& (&ARGLIST[argn])->ae_fnum != curbuf->b_fnum
&& !check_can_set_curbuf_forceit(eap->forceit)) {
return;
}
setpcmark();
// split window or create new tab page first
if (*eap->cmd == 's' || cmdmod.cmod_tab != 0) {
if (is_split_cmd || cmdmod.cmod_tab != 0) {
if (win_split(0, 0) == FAIL) {
return;
}

View File

@ -1305,6 +1305,12 @@ int do_buffer(int action, int start, int dir, int count, int forceit)
}
return FAIL;
}
if (action == DOBUF_GOTO && buf != curbuf && !check_can_set_curbuf_forceit(forceit)) {
// disallow navigating to another buffer when 'winfixbuf' is applied
return FAIL;
}
if ((action == DOBUF_GOTO || action == DOBUF_SPLIT) && (buf->b_flags & BF_DUMMY)) {
// disallow navigating to the dummy buffer
semsg(_(e_nobufnr), count);

View File

@ -139,6 +139,8 @@ typedef struct {
#define w_ve_flags w_onebuf_opt.wo_ve_flags // flags for 'virtualedit'
OptInt wo_nuw;
#define w_p_nuw w_onebuf_opt.wo_nuw // 'numberwidth'
int wo_wfb;
#define w_p_wfb w_onebuf_opt.wo_wfb // 'winfixbuf'
int wo_wfh;
#define w_p_wfh w_onebuf_opt.wo_wfh // 'winfixheight'
int wo_wfw;

View File

@ -2008,6 +2008,10 @@ static int check_readonly(int *forceit, buf_T *buf)
/// GETFILE_OPEN_OTHER for successfully opening another file.
int getfile(int fnum, char *ffname_arg, char *sfname_arg, bool setpm, linenr_T lnum, bool forceit)
{
if (!check_can_set_curbuf_forceit(forceit)) {
return GETFILE_ERROR;
}
char *ffname = ffname_arg;
char *sfname = sfname_arg;
bool other;

View File

@ -812,7 +812,7 @@ module.cmds = {
},
{
command = 'drop',
flags = bit.bor(FILES, CMDARG, NEEDARG, ARGOPT, TRLBAR),
flags = bit.bor(BANG, FILES, CMDARG, NEEDARG, ARGOPT, TRLBAR),
addr_type = 'ADDR_NONE',
func = 'ex_drop',
},

View File

@ -444,6 +444,27 @@ int buf_write_all(buf_T *buf, bool forceit)
/// ":argdo", ":windo", ":bufdo", ":tabdo", ":cdo", ":ldo", ":cfdo" and ":lfdo"
void ex_listdo(exarg_T *eap)
{
if (curwin->w_p_wfb) {
if ((eap->cmdidx == CMD_ldo || eap->cmdidx == CMD_lfdo) && !eap->forceit) {
// Disallow :ldo if 'winfixbuf' is applied
semsg("%s", e_winfixbuf_cannot_go_to_buffer);
return;
}
if (win_valid(prevwin)) {
// Change the current window to another because 'winfixbuf' is enabled
curwin = prevwin;
} else {
// Split the window, which will be 'nowinfixbuf', and set curwin to that
exarg_T new_eap = {
.cmdidx = CMD_split,
.cmd = "split",
.arg = "",
};
ex_splitview(&new_eap);
}
}
char *save_ei = NULL;
// Temporarily override SHM_OVER and SHM_OVERALL to avoid that file

View File

@ -5334,6 +5334,10 @@ static void ex_resize(exarg_T *eap)
/// ":find [+command] <file>" command.
static void ex_find(exarg_T *eap)
{
if (!check_can_set_curbuf_forceit(eap->forceit)) {
return;
}
char *file_to_find = NULL;
char *search_ctx = NULL;
char *fname = find_file_in_path(eap->arg, strlen(eap->arg),
@ -5364,6 +5368,14 @@ static void ex_find(exarg_T *eap)
/// ":edit", ":badd", ":balt", ":visual".
static void ex_edit(exarg_T *eap)
{
// Exclude commands which keep the window's current buffer
if (eap->cmdidx != CMD_badd
&& eap->cmdidx != CMD_balt
// All other commands must obey 'winfixbuf' / ! rules
&& !check_can_set_curbuf_forceit(eap->forceit)) {
return;
}
do_exedit(eap, NULL);
}
@ -6670,7 +6682,7 @@ static void ex_checkpath(exarg_T *eap)
{
find_pattern_in_path(NULL, 0, 0, false, false, CHECK_PATH, 1,
eap->forceit ? ACTION_SHOW_ALL : ACTION_SHOW,
1, (linenr_T)MAXLNUM);
1, (linenr_T)MAXLNUM, eap->forceit);
}
/// ":psearch"
@ -6729,7 +6741,7 @@ static void ex_findpat(exarg_T *eap)
if (!eap->skip) {
find_pattern_in_path(eap->arg, 0, strlen(eap->arg), whole, !eap->forceit,
*eap->cmd == 'd' ? FIND_DEFINE : FIND_ANY,
n, action, eap->line1, eap->line2);
n, action, eap->line1, eap->line2, eap->forceit);
}
}

View File

@ -971,6 +971,9 @@ EXTERN const char e_val_too_large[] INIT(= N_("E1510: Value too large: %s"));
EXTERN const char e_undobang_cannot_redo_or_move_branch[]
INIT(= N_("E5767: Cannot use :undo! to redo or move to a different undo branch"));
EXTERN const char e_winfixbuf_cannot_go_to_buffer[]
INIT(= N_("E1513: Cannot edit buffer. 'winfixbuf' is enabled"));
EXTERN const char e_trustfile[] INIT(= N_("E5570: Cannot update trust file: %s"));
EXTERN const char e_unknown_option2[] INIT(= N_("E355: Unknown option: %s"));

View File

@ -3027,7 +3027,7 @@ static void get_next_include_file_completion(int compl_type)
((compl_type == CTRL_X_PATH_DEFINES
&& !(compl_cont_status & CONT_SOL))
? FIND_DEFINE : FIND_ANY),
1, ACTION_EXPAND, 1, MAXLNUM);
1, ACTION_EXPAND, 1, MAXLNUM, false);
}
/// Get the next set of words matching "compl_pattern" in dictionary or

View File

@ -3896,6 +3896,10 @@ static void nv_gotofile(cmdarg_T *cap)
return;
}
if (!check_can_set_curbuf_disabled()) {
return;
}
char *ptr = grab_file_name(cap->count1, &lnum);
if (ptr != NULL) {
@ -4232,7 +4236,8 @@ static void nv_brackets(cmdarg_T *cap)
(cap->cmdchar == ']'
? curwin->w_cursor.lnum + 1
: 1),
MAXLNUM);
MAXLNUM,
false);
xfree(ptr);
curwin->w_set_curswant = true;
}

View File

@ -4629,6 +4629,8 @@ void *get_varp_from(vimoption_T *p, buf_T *buf, win_T *win)
return &(win->w_p_rnu);
case PV_NUW:
return &(win->w_p_nuw);
case PV_WFB:
return &(win->w_p_wfb);
case PV_WFH:
return &(win->w_p_wfh);
case PV_WFW:

View File

@ -8406,6 +8406,8 @@ return {
"split" when both are present.
uselast If included, jump to the previously used window when
jumping to errors with |quickfix| commands.
If a window has 'winfixbuf' enabled, 'switchbuf' is currently not
applied to the split window.
]=],
expand_cb = 'expand_set_switchbuf',
full_name = 'switchbuf',
@ -9816,6 +9818,23 @@ return {
type = 'number',
varname = 'p_window',
},
{
abbreviation = 'wfb',
defaults = { if_true = false },
desc = [=[
If enabled, the buffer and any window that displays it are paired.
For example, attempting to change the buffer with |:edit| will fail.
Other commands which change a window's buffer such as |:cnext| will
also skip any window with 'winfixbuf' enabled. However if a command
has an "!" option, a window can be forced to switch buffers.
]=],
full_name = 'winfixbuf',
pv_name = 'p_wfb',
redraw = { 'current_window' },
scope = { 'window' },
short_desc = N_('pin a window to a specific buffer'),
type = 'boolean',
},
{
abbreviation = 'wfh',
defaults = { if_true = false },

View File

@ -2699,7 +2699,7 @@ static void qf_goto_win_with_qfl_file(int qf_fnum)
// Didn't find it, go to the window before the quickfix
// window, unless 'switchbuf' contains 'uselast': in this case we
// try to jump to the previously used window first.
if ((swb_flags & SWB_USELAST) && win_valid(prevwin)) {
if ((swb_flags & SWB_USELAST) && !prevwin->w_p_wfb && win_valid(prevwin)) {
win = prevwin;
} else if (altwin != NULL) {
win = altwin;
@ -2714,6 +2714,7 @@ static void qf_goto_win_with_qfl_file(int qf_fnum)
// Remember a usable window.
if (altwin == NULL
&& !win->w_p_pvw
&& !win->w_p_wfb
&& bt_normal(win->w_buffer)) {
altwin = win;
}
@ -2802,6 +2803,25 @@ static int qf_jump_edit_buffer(qf_info_T *qi, qfline_T *qf_ptr, int forceit, int
ECMD_HIDE + ECMD_SET_HELP,
prev_winid == curwin->handle ? curwin : NULL);
} else {
if (!forceit && curwin->w_p_wfb) {
if (qi->qfl_type == QFLT_LOCATION) {
// Location lists cannot split or reassign their window
// so 'winfixbuf' windows must fail
semsg("%s", e_winfixbuf_cannot_go_to_buffer);
return QF_ABORT;
}
if (!win_valid(prevwin)) {
// Split the window, which will be 'nowinfixbuf', and set curwin to that
exarg_T new_eap = {
.cmdidx = CMD_split,
.cmd = "split",
.arg = "",
};
ex_splitview(&new_eap);
}
}
retval = buflist_getfile(qf_ptr->qf_fnum, 1,
GETF_SETMARK | GETF_SWITCH, forceit);
}
@ -4297,6 +4317,11 @@ static void qf_jump_first(qf_info_T *qi, unsigned save_qfid, int forceit)
if (qf_restore_list(qi, save_qfid) == FAIL) {
return;
}
if (!check_can_set_curbuf_forceit(forceit)) {
return;
}
// Autocommands might have cleared the list, check for that
if (!qf_list_empty(qf_get_curlist(qi))) {
qf_jump(qi, 0, 0, forceit);
@ -5125,7 +5150,7 @@ void ex_cfile(exarg_T *eap)
// This function is used by the :cfile, :cgetfile and :caddfile
// commands.
// :cfile always creates a new quickfix list and jumps to the
// :cfile always creates a new quickfix list and may jump to the
// first error.
// :cgetfile creates a new quickfix list but doesn't jump to the
// first error.
@ -5587,6 +5612,10 @@ theend:
/// ":lvimgrepadd {pattern} file(s)"
void ex_vimgrep(exarg_T *eap)
{
if (!check_can_set_curbuf_forceit(eap->forceit)) {
return;
}
char *au_name = vgr_get_auname(eap->cmdidx);
if (au_name != NULL && apply_autocmds(EVENT_QUICKFIXCMDPRE, au_name,
curbuf->b_fname, true, curbuf)) {

View File

@ -3564,8 +3564,10 @@ static char *get_line_and_copy(linenr_T lnum, char *buf)
/// @param action What to do when we find it
/// @param start_lnum first line to start searching
/// @param end_lnum last line for searching
/// @param forceit If true, always switch to the found path
void find_pattern_in_path(char *ptr, Direction dir, size_t len, bool whole, bool skip_comments,
int type, int count, int action, linenr_T start_lnum, linenr_T end_lnum)
int type, int count, int action, linenr_T start_lnum, linenr_T end_lnum,
int forceit)
{
SearchedFile *files; // Stack of included files
SearchedFile *bigger; // When we need more space
@ -4025,7 +4027,7 @@ search_line:
break;
}
if (!GETFILE_SUCCESS(getfile(curwin_save->w_buffer->b_fnum, NULL,
NULL, true, lnum, false))) {
NULL, true, lnum, forceit))) {
break; // failed to jump to file
}
} else {
@ -4035,7 +4037,7 @@ search_line:
check_cursor();
} else {
if (!GETFILE_SUCCESS(getfile(0, files[depth].name, NULL, true,
files[depth].lnum, false))) {
files[depth].lnum, forceit))) {
break; // failed to jump to file
}
// autocommands may have changed the lnum, we don't

View File

@ -290,6 +290,10 @@ void set_buflocal_tfu_callback(buf_T *buf)
/// @param verbose print "tag not found" message
void do_tag(char *tag, int type, int count, int forceit, bool verbose)
{
if (postponed_split == 0 && !check_can_set_curbuf_forceit(forceit)) {
return;
}
taggy_T *tagstack = curwin->w_tagstack;
int tagstackidx = curwin->w_tagstackidx;
int tagstacklen = curwin->w_tagstacklen;
@ -2784,6 +2788,10 @@ static char *tag_full_fname(tagptrs_T *tagp)
/// @return OK for success, NOTAGFILE when file not found, FAIL otherwise.
static int jumpto_tag(const char *lbuf_arg, int forceit, bool keep_help)
{
if (postponed_split == 0 && !check_can_set_curbuf_forceit(forceit)) {
return FAIL;
}
char *pbuf_end;
char *tofree_fname = NULL;
tagptrs_T tagp;

View File

@ -133,6 +133,35 @@ static void log_frame_layout(frame_T *frame)
}
#endif
/// Check if the current window is allowed to move to a different buffer.
///
/// @return If the window has 'winfixbuf', or this function will return false.
bool check_can_set_curbuf_disabled(void)
{
if (curwin->w_p_wfb) {
semsg("%s", e_winfixbuf_cannot_go_to_buffer);
return false;
}
return true;
}
/// Check if the current window is allowed to move to a different buffer.
///
/// @param forceit If true, do not error. If false and 'winfixbuf' is enabled, error.
///
/// @return If the window has 'winfixbuf', then forceit must be true
/// or this function will return false.
bool check_can_set_curbuf_forceit(int forceit)
{
if (!forceit && curwin->w_p_wfb) {
semsg("%s", e_winfixbuf_cannot_go_to_buffer);
return false;
}
return true;
}
/// @return the current window, unless in the cmdline window and "prevwin" is
/// set, then return "prevwin".
win_T *prevwin_curwin(void)
@ -597,7 +626,7 @@ wingotofile:
ptr = xmemdupz(ptr, len);
find_pattern_in_path(ptr, 0, len, true, Prenum == 0,
type, Prenum1, ACTION_SPLIT, 1, MAXLNUM);
type, Prenum1, ACTION_SPLIT, 1, MAXLNUM, false);
xfree(ptr);
curwin->w_set_curswant = true;
break;

View File

@ -0,0 +1,54 @@
local helpers = require('test.functional.helpers')(after_each)
local clear = helpers.clear
local exec_lua = helpers.exec_lua
describe("Nvim API calls with 'winfixbuf'", function()
before_each(function()
clear()
end)
it("Calling vim.api.nvim_win_set_buf with 'winfixbuf'", function()
local results = exec_lua([[
local function _setup_two_buffers()
local buffer = vim.api.nvim_create_buf(true, true)
vim.api.nvim_create_buf(true, true) -- Make another buffer
local current_window = 0
vim.api.nvim_set_option_value("winfixbuf", true, {win=current_window})
return buffer
end
local other_buffer = _setup_two_buffers()
local current_window = 0
local results, _ = pcall(vim.api.nvim_win_set_buf, current_window, other_buffer)
return results
]])
assert(results == false)
end)
it("Calling vim.api.nvim_set_current_buf with 'winfixbuf'", function()
local results = exec_lua([[
local function _setup_two_buffers()
local buffer = vim.api.nvim_create_buf(true, true)
vim.api.nvim_create_buf(true, true) -- Make another buffer
local current_window = 0
vim.api.nvim_set_option_value("winfixbuf", true, {win=current_window})
return buffer
end
local other_buffer = _setup_two_buffers()
local results, _ = pcall(vim.api.nvim_set_current_buf, other_buffer)
return results
]])
assert(results == false)
end)
end)

File diff suppressed because it is too large Load Diff