Commit: patch 9.2.0338: Cannot handle mouseclicks in the tabline

3 views
Skip to first unread message

Christian Brabandt

unread,
Apr 11, 2026, 11:32:24 AM (13 hours ago) Apr 11
to vim...@googlegroups.com
patch 9.2.0338: Cannot handle mouseclicks in the tabline

Commit: https://github.com/vim/vim/commit/0802e00f2a57fb62a0012a2f15a718e72ed806fd
Author: Yasuhiro Matsumoto <matt...@gmail.com>
Date: Sat Apr 11 15:22:24 2026 +0000

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 <matt...@gmail.com>
Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index 300eebbc2..f083d6ff1 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -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)
diff --git a/src/globals.h b/src/globals.h
index b7533c632..7d873eea4 100644
--- a/src/globals.h
+++ b/src/globals.h
@@ -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);
diff --git a/src/mouse.c b/src/mouse.c
index 6bff3d7c2..1077c5ee7 100644
--- a/src/mouse.c
+++ b/src/mouse.c
@@ -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

/*
diff --git a/src/screen.c b/src/screen.c
index f667e6eb2..ed28ac91a 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -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;
}
}

diff --git a/src/testdir/test_statusline.vim b/src/testdir/test_statusline.vim
index 673a6ed12..27fef946a 100644
--- a/src/testdir/test_statusline.vim
+++ b/src/testdir/test_statusline.vim
@@ -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
diff --git a/src/version.c b/src/version.c
index f4e5ef7ec..15f52077e 100644
--- a/src/version.c
+++ b/src/version.c
@@ -734,6 +734,8 @@ static char *(features[]) =

static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 338,
/**/
337,
/**/
Reply all
Reply to author
Forward
0 new messages