patch 9.2.0338: Cannot handle mouseclicks in the tabline

Problem:  Cannot handle mouseclicks in the tabline
Solution: Support %[FuncName] click regions in 'tabline', add "area" key
          to the click info dict (Yasuhiro Matsumoto).

The previous implementation resolved and stored click regions only for
per-window statuslines; the tabline path in win_redr_custom() (wp==NULL)
parsed %[FuncName] but discarded the regions, and tabline clicks were
dispatched via TabPageIdxs[] which didn't know about them.

Add a global tabline_stl_click array populated from the tabline path,
refactor stl_click_handler() to take the regions directly, and dispatch
matching clicks from do_mouse() before falling through to tab selection.
The winid entry in the callback dict is 0 for tabline clicks.

related: #19841
closes:  #19950

Supported by AI.

Signed-off-by: Yasuhiro Matsumoto <mattn.jp@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Yasuhiro Matsumoto
2026-04-11 15:22:24 +00:00
committed by Christian Brabandt
parent 8fd37e42a6
commit 0802e00f2a
6 changed files with 165 additions and 29 deletions
+11 -4
View File
@@ -1,4 +1,4 @@
*options.txt* For Vim version 9.2. Last change: 2026 Apr 09
*options.txt* For Vim version 9.2. Last change: 2026 Apr 11
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -8626,7 +8626,8 @@ A jump table for the options with a short description can be found at |Q_op|.
*stl-%[FuncName]*
%[ defines clickable regions in the statusline. When the user clicks
on a region with the mouse, the specified function is called.
on a region with the mouse, the specified function is called. The
same syntax can also be used in 'tabline'.
%[FuncName] Start of a clickable region. "FuncName" is the name
of a Vim function to call when the region is clicked.
@@ -8644,11 +8645,17 @@ A jump table for the options with a short description can be found at |Q_op|.
"button" Mouse button: "l" (left), "m" (middle), "r" (right).
"mods" Modifier keys: combination of "s" (shift), "c" (ctrl),
"a" (alt). Empty string if no modifiers.
"winid" |window-ID| of the window whose statusline was clicked.
"winid" |window-ID| of the window whose statusline was clicked,
or 0 when the click was in 'tabline'.
"area" "statusline" or "tabline". Indicates which option the
clicked region belongs to. Useful when a single
callback is shared between 'statusline' and 'tabline'.
If the function returns non-zero, the statusline is redrawn.
Dragging the statusline to resize the window still works even when
click handlers are defined.
click handlers are defined. When used in 'tabline', clicks in
%[FuncName] regions are dispatched to the callback instead of the
default tab-selection behavior.
Example: >
func! ClickFile(info)
+4
View File
@@ -104,6 +104,10 @@ EXTERN int redrawing_for_callback INIT(= 0);
*/
EXTERN short *TabPageIdxs INIT(= NULL);
// Click regions for 'tabline' (%[FuncName]).
EXTERN stl_click_region_T *tabline_stl_click INIT(= NULL);
EXTERN int tabline_stl_click_count INIT(= 0);
#ifdef FEAT_PROP_POPUP
// Array with size Rows x Columns containing zindex of popups.
EXTERN short *popup_mask INIT(= NULL);
+59 -11
View File
@@ -21,6 +21,9 @@ static long mouse_hor_step = 6;
static long mouse_vert_step = 3;
static win_T *dragwin = NULL; // window being dragged
static int stl_click_handler(win_T *wp, int mcol, int which_button, int mods);
static int stl_click_handler_regions(stl_click_region_T *regions,
int region_count, int winid, int mcol,
int which_button, int mods);
void
mouse_set_vert_scroll_step(long step)
@@ -502,6 +505,16 @@ do_mouse(
// Check for clicking in the tab page line.
if (TabPageIdxs != NULL && mouse_row == 0 && firstwin->w_winrow > 0)
{
// Dispatch 'tabline' %[FuncName] click regions before falling through
// to tab-page selection. On drag events fall through to the normal
// tab-drag handling.
if (is_click && !is_drag
&& stl_click_handler_regions(tabline_stl_click,
tabline_stl_click_count,
0, mouse_col, which_button,
mod_mask))
return FALSE;
tp_label.just_in = true;
tp_label.nr = TabPageIdxs[mouse_col];
@@ -1640,11 +1653,19 @@ mouse_model_popup(void)
}
/*
* Call a statusline click handler function.
* Call a click-region callback function.
* "regions"/"region_count" describe the resolved click regions,
* "winid" is stored as the "winid" key in the info dict (0 for tabline).
* Returns TRUE if the function was called and handled the click.
*/
static int
stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
stl_click_handler_regions(
stl_click_region_T *regions,
int region_count,
int winid,
int mcol,
int which_button,
int mods)
{
#ifdef FEAT_EVAL
int n;
@@ -1658,17 +1679,16 @@ stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
funcexe_T funcexe;
int col = mcol;
if (wp == NULL || wp->w_stl_click == NULL || wp->w_stl_click_count == 0)
if (regions == NULL || region_count == 0)
return FALSE;
// Find the click region at the given column.
for (n = 0; n < wp->w_stl_click_count; n++)
for (n = 0; n < region_count; n++)
{
if (col >= wp->w_stl_click[n].col_start
&& col < wp->w_stl_click[n].col_end)
if (col >= regions[n].col_start && col < regions[n].col_end)
break;
}
if (n >= wp->w_stl_click_count || wp->w_stl_click[n].funcname == NULL)
if (n >= region_count || regions[n].funcname == NULL)
return FALSE;
// Build the info dictionary.
@@ -1676,7 +1696,7 @@ stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
if (info == NULL)
return FALSE;
dict_add_number(info, "minwid", wp->w_stl_click[n].minwid);
dict_add_number(info, "minwid", regions[n].minwid);
// Determine number of clicks.
// MOD_MASK_2CLICK=0x20, MOD_MASK_3CLICK=0x40, MOD_MASK_4CLICK=0x60
@@ -1705,7 +1725,13 @@ stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
mods_str[mi] = NUL;
dict_add_string(info, "mods", mods_str);
dict_add_number(info, "winid", wp->w_id);
dict_add_number(info, "winid", winid);
// "area": which option the clicked region belongs to. Lets a shared
// dispatcher distinguish 'statusline' from 'tabline' (and future areas)
// without having to overload winid == 0.
dict_add_string(info, "area",
winid == 0 ? (char_u *)"tabline" : (char_u *)"statusline");
// Call the function with the info dict as argument.
argvars[0].v_type = VAR_DICT;
@@ -1718,7 +1744,7 @@ stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
CLEAR_FIELD(funcexe);
funcexe.fe_evaluate = TRUE;
(void)call_func(wp->w_stl_click[n].funcname, -1,
(void)call_func(regions[n].funcname, -1,
&rettv, 1, argvars, &funcexe);
n = (int)rettv.vval.v_number;
@@ -1726,11 +1752,20 @@ stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
dict_unref(info);
if (n != 0)
{
// Make sure the tabline gets redrawn too when the callback asks for
// a redraw (redraw_statuslines() only redraws the tabline when
// redraw_tabline is set).
if (winid == 0)
redraw_tabline = TRUE;
redraw_statuslines();
}
return TRUE;
#else
(void)wp;
(void)regions;
(void)region_count;
(void)winid;
(void)mcol;
(void)which_button;
(void)mods;
@@ -1738,6 +1773,19 @@ stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
#endif
}
/*
* Call a statusline click handler function for window "wp".
* Returns TRUE if the function was called and handled the click.
*/
static int
stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
{
if (wp == NULL)
return FALSE;
return stl_click_handler_regions(wp->w_stl_click, wp->w_stl_click_count,
wp->w_id, mcol, which_button, mods);
}
// dragwin is declared near the top of the file
/*
+32 -14
View File
@@ -1486,23 +1486,41 @@ win_redr_custom(
TabPageIdxs[col++] = fillchar;
}
// Resolve click function regions for statusline.
if (wp != NULL && !draw_ruler)
// Resolve click function regions for statusline or tabline.
if (!draw_ruler)
{
int click_count = 0;
stl_click_region_T **out_regions;
int *out_count;
int base_col;
int click_count = 0;
if (wp != NULL)
{
out_regions = &wp->w_stl_click;
out_count = &wp->w_stl_click_count;
base_col = wp->w_wincol;
}
else
{
// 'tabline': store regions in global state since there is no
// associated window.
out_regions = &tabline_stl_click;
out_count = &tabline_stl_click_count;
base_col = firstwin->w_wincol;
}
// Count the click regions.
for (n = 0; clicktab[n].start != NULL; n++)
click_count++;
// Free old click regions.
if (wp->w_stl_click != NULL)
if (*out_regions != NULL)
{
for (n = 0; n < wp->w_stl_click_count; n++)
vim_free(wp->w_stl_click[n].funcname);
VIM_CLEAR(wp->w_stl_click);
for (n = 0; n < *out_count; n++)
vim_free((*out_regions)[n].funcname);
VIM_CLEAR(*out_regions);
}
wp->w_stl_click_count = 0;
*out_count = 0;
if (click_count > 0)
{
@@ -1514,7 +1532,7 @@ win_redr_custom(
{
char_u *cur_funcname = NULL;
int cur_minwid = 0;
int region_start = wp->w_wincol;
int region_start = base_col;
// Walk through click records converting buffer positions
// to screen columns.
@@ -1530,7 +1548,7 @@ win_redr_custom(
if (cur_funcname != NULL)
{
regions[rcount].col_start = region_start;
regions[rcount].col_end = wp->w_wincol + len;
regions[rcount].col_end = base_col + len;
regions[rcount].funcname =
vim_strsave(cur_funcname);
regions[rcount].minwid = cur_minwid;
@@ -1539,22 +1557,22 @@ win_redr_custom(
cur_funcname = clicktab[n].funcname;
cur_minwid = clicktab[n].minwid;
region_start = wp->w_wincol + len;
region_start = base_col + len;
}
// Close final region if it extends to the end.
if (cur_funcname != NULL)
{
regions[rcount].col_start = region_start;
regions[rcount].col_end = wp->w_wincol + maxwidth;
regions[rcount].col_end = base_col + maxwidth;
regions[rcount].funcname =
vim_strsave(cur_funcname);
regions[rcount].minwid = cur_minwid;
rcount++;
}
wp->w_stl_click = regions;
wp->w_stl_click_count = rcount;
*out_regions = regions;
*out_count = rcount;
}
}
+57
View File
@@ -752,6 +752,7 @@ func Test_statusline_click_handler()
call assert_equal(1, g:stl_click_info.nclicks)
call assert_equal(0, g:stl_click_info.minwid)
call assert_equal(win_getid(), g:stl_click_info.winid)
call assert_equal('statusline', g:stl_click_info.area)
unlet! g:stl_click_info
" Click outside click region (on the filename part)
@@ -873,4 +874,60 @@ func Test_statusline_click_linebreak_still_works()
let &laststatus = save_ls
endfunc
func Test_tabline_click_handler()
let save_mouse = &mouse
let save_tal = &tabline
let save_stal = &showtabline
if has('gui')
let save_go = &guioptions
set guioptions-=e
endif
set mouse=a
set showtabline=2
" Two adjacent click regions in 'tabline' with different minwid.
set tabline=%1[StlClickTestFunc][AAA]%[]%2[StlClickTestFunc][BBB]%[]
redraw!
" Click on [AAA] region (tabline is row 1).
call test_setmouse(1, 2)
call feedkeys("\<LeftMouse>\<LeftRelease>", 'xt')
call assert_true(exists('g:stl_click_info'))
call assert_equal('l', g:stl_click_info.button)
call assert_equal(1, g:stl_click_info.nclicks)
call assert_equal(1, g:stl_click_info.minwid)
" winid is 0 for tabline clicks (no associated window).
call assert_equal(0, g:stl_click_info.winid)
call assert_equal('tabline', g:stl_click_info.area)
unlet! g:stl_click_info
" Click on [BBB] region.
call test_setmouse(1, 7)
call feedkeys("\<LeftMouse>\<LeftRelease>", 'xt')
call assert_true(exists('g:stl_click_info'))
call assert_equal(2, g:stl_click_info.minwid)
unlet! g:stl_click_info
" Middle click on [AAA].
call test_setmouse(1, 2)
call feedkeys("\<MiddleMouse>\<MiddleRelease>", 'xt')
call assert_true(exists('g:stl_click_info'))
call assert_equal('m', g:stl_click_info.button)
unlet! g:stl_click_info
" Click outside any %[...] region: no callback, no error.
set tabline=xxx%1[StlClickTestFunc][YYY]%[]
redraw!
call test_setmouse(1, 1)
call feedkeys("\<LeftMouse>\<LeftRelease>", 'xt')
call assert_false(exists('g:stl_click_info'))
let &mouse = save_mouse
let &tabline = save_tal
let &showtabline = save_stal
if has('gui')
let &guioptions = save_go
endif
endfunc
" vim: shiftwidth=2 sts=2 expandtab
+2
View File
@@ -734,6 +734,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
338,
/**/
337,
/**/