patch 9.2.0360: Cannot handle mouse-clicks in the tabpanel
Commit:
https://github.com/vim/vim/commit/1c299f26316cde3f32ec95b7c440e868ada0cb20
Author: Yasuhiro Matsumoto <
matt...@gmail.com>
Date: Thu Apr 16 20:29:33 2026 +0000
patch 9.2.0360: Cannot handle mouse-clicks in the tabpanel
Problem: Cannot handle mouse-clicks in the tabpanel
Solution: Add support using the %[FuncName] atom for the tabpanel
(Yasuhiro Matsumoto)
Extend the statusline/tabline click region mechanism to work with
'tabpanel'. The callback receives a dict with "area" set to "tabpanel"
and a "tabnr" key indicating which tab page label was clicked.
closes: #19960
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 31a9629c0..62d7ef8df 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 15
+*options.txt* For Vim version 9.2. Last change: 2026 Apr 16
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -8689,7 +8689,7 @@ 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. The
- same syntax can also be used in 'tabline'.
+ same syntax can also be used in 'tabline' and 'tabpanel'.
%[FuncName] Start of a clickable region. "FuncName" is the name
of a Vim function to call when the region is clicked.
@@ -8708,16 +8708,16 @@ A jump table for the options with a short description can be found at |Q_op|.
"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,
- 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'.
+ or 0 when the click was in 'tabline' or 'tabpanel'.
+ "area" "statusline", "tabline", or "tabpanel". Indicates
+ which option the clicked region belongs to.
+ "tabnr" (tabpanel only) Tab page number of the clicked label.
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. When used in 'tabline', clicks in
- %[FuncName] regions are dispatched to the callback instead of the
- default tab page selection behavior.
+ click handlers are defined. When used in 'tabline' or 'tabpanel',
+ clicks in %[FuncName] regions are dispatched to the callback
+ instead of the default tab-selection behavior.
Example: >
func! ClickFile(info)
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index ee24e2901..7fa53f87d 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -1,4 +1,4 @@
-*version9.txt* For Vim version 9.2. Last change: 2026 Apr 15
+*version9.txt* For Vim version 9.2. Last change: 2026 Apr 16
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -52613,8 +52613,8 @@ Other ~
pairs individually (e.g. 'listchars', 'fillchars', 'diffopt').
- |system()| and |systemlist()| functions accept a list as first argument,
bypassing the shell completely.
-- Allow mouse clickable regions in the |status-line| using the
- |stl-%[FuncName]| atom.
+- Allow mouse clickable regions in the 'statusline', 'tabline' and the
+ 'tabpanel' using the |stl-%[FuncName]| atom.
- Enable reflow support in the |:terminal|.
Platform specific ~
diff --git a/src/globals.h b/src/globals.h
index 20450aa68..9a6c28593 100644
--- a/src/globals.h
+++ b/src/globals.h
@@ -108,6 +108,10 @@ EXTERN short *TabPageIdxs INIT(= NULL);
EXTERN stl_click_region_T *tabline_stl_click INIT(= NULL);
EXTERN int tabline_stl_click_count INIT(= 0);
+// Click regions for 'tabpanel' (%[FuncName]).
+EXTERN stl_click_region_T *tabpanel_stl_click INIT(= NULL);
+EXTERN int tabpanel_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 1077c5ee7..45ffd4b51 100644
--- a/src/mouse.c
+++ b/src/mouse.c
@@ -20,9 +20,11 @@
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(win_T *wp, int mrow, 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 region_count, int winid,
+ char_u *area_name, int mrow, int mcol,
int which_button, int mods);
void
@@ -492,6 +494,17 @@ do_mouse(
if (mouse_col < firstwin->w_wincol
|| mouse_col >= firstwin->w_wincol + topframe->fr_width)
{
+ // Dispatch 'tabpanel' %[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(tabpanel_stl_click,
+ tabpanel_stl_click_count,
+ 0, (char_u *)"tabpanel",
+ mouse_row, mouse_col,
+ which_button, mod_mask))
+ return FALSE;
+
tp_label.is_panel = true;
tp_label.just_in = true;
tp_label.nr = get_tabpagenr_on_tabpanel();
@@ -511,8 +524,9 @@ do_mouse(
if (is_click && !is_drag
&& stl_click_handler_regions(tabline_stl_click,
tabline_stl_click_count,
- 0, mouse_col, which_button,
- mod_mask))
+ 0, (char_u *)"tabline",
+ mouse_row, mouse_col,
+ which_button, mod_mask))
return FALSE;
tp_label.just_in = true;
@@ -778,7 +792,7 @@ do_mouse(
// Check for statusline click handler early, before visual mode or
// other button-specific handling can interfere.
if (in_status_line && is_click && !is_drag
- && stl_click_handler(dragwin, mouse_col,
+ && stl_click_handler(dragwin, mouse_row, mouse_col,
which_button, mod_mask))
{
#ifdef FEAT_MOUSESHAPE
@@ -1663,6 +1677,8 @@ stl_click_handler_regions(
stl_click_region_T *regions,
int region_count,
int winid,
+ char_u *area_name,
+ int mrow,
int mcol,
int which_button,
int mods)
@@ -1682,10 +1698,12 @@ stl_click_handler_regions(
if (regions == NULL || region_count == 0)
return FALSE;
- // Find the click region at the given column.
+ // Find the click region at the given row and column.
for (n = 0; n < region_count; n++)
{
- if (col >= regions[n].col_start && col < regions[n].col_end)
+ if (regions[n].row == mrow
+ && col >= regions[n].col_start
+ && col < regions[n].col_end)
break;
}
if (n >= region_count || regions[n].funcname == NULL)
@@ -1728,10 +1746,13 @@ stl_click_handler_regions(
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");
+ // dispatcher distinguish 'statusline', 'tabline' and 'tabpanel' without
+ // having to overload winid == 0.
+ dict_add_string(info, "area", area_name);
+
+ // Expose tab page number for 'tabpanel' regions.
+ if (regions[n].tabnr > 0)
+ dict_add_number(info, "tabnr", regions[n].tabnr);
// Call the function with the info dict as argument.
argvars[0].v_type = VAR_DICT;
@@ -1755,9 +1776,14 @@ stl_click_handler_regions(
{
// 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).
+ // redraw_tabline is set). For tabpanel the whole screen needs to be
+ // refreshed.
if (winid == 0)
redraw_tabline = TRUE;
+# ifdef FEAT_TABPANEL
+ if (STRCMP(area_name, "tabpanel") == 0)
+ redraw_all_later(UPD_NOT_VALID);
+# endif
redraw_statuslines();
}
@@ -1766,6 +1792,8 @@ stl_click_handler_regions(
(void)regions;
(void)region_count;
(void)winid;
+ (void)area_name;
+ (void)mrow;
(void)mcol;
(void)which_button;
(void)mods;
@@ -1778,12 +1806,13 @@ stl_click_handler_regions(
* 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(win_T *wp, int mrow, 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);
+ wp->w_id, (char_u *)"statusline",
+ mrow, mcol, which_button, mods);
}
// dragwin is declared near the top of the file
diff --git a/src/screen.c b/src/screen.c
index eb7cc9cdd..b9bde1d57 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -1492,8 +1492,13 @@ win_redr_custom(
stl_click_region_T **out_regions;
int *out_count;
int base_col;
+ int base_row;
int click_count = 0;
+ // clicktab reflects the last iteration of the draw loop above, so
+ // the regions belong to the last drawn row.
+ base_row = row + stlh_cnt - 1;
+
if (wp != NULL)
{
out_regions = &wp->w_stl_click;
@@ -1547,11 +1552,13 @@ win_redr_custom(
// Close previous region if there was one.
if (cur_funcname != NULL)
{
+ regions[rcount].row = base_row;
regions[rcount].col_start = region_start;
regions[rcount].col_end = base_col + len;
regions[rcount].funcname =
vim_strsave(cur_funcname);
regions[rcount].minwid = cur_minwid;
+ regions[rcount].tabnr = 0;
rcount++;
}
@@ -1563,11 +1570,13 @@ win_redr_custom(
// Close final region if it extends to the end.
if (cur_funcname != NULL)
{
+ regions[rcount].row = base_row;
regions[rcount].col_start = region_start;
regions[rcount].col_end = base_col + maxwidth;
regions[rcount].funcname =
vim_strsave(cur_funcname);
regions[rcount].minwid = cur_minwid;
+ regions[rcount].tabnr = 0;
rcount++;
}
diff --git a/src/structs.h b/src/structs.h
index 648ceec90..e76c651d2 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -1451,10 +1451,12 @@ typedef struct {
* Per-window resolved click regions (screen column based).
*/
typedef struct {
+ int row; // screen row where region lives
int col_start; // screen column where region starts
int col_end; // screen column where region ends
char_u *funcname; // function name (allocated copy)
int minwid; // minwid value
+ int tabnr; // tab page number (tabpanel only, 0 otherwise)
} stl_click_region_T;
diff --git a/src/tabpanel.c b/src/tabpanel.c
index 51929a8b1..4138f9760 100644
--- a/src/tabpanel.c
+++ b/src/tabpanel.c
@@ -17,6 +17,9 @@
static void do_by_tplmode(int tplmode, int col_start, int col_end,
int *pcurtab_row, int *ptabpagenr);
+static void tabpanel_free_click_regions(void);
+static void tabpanel_append_click_regions(stl_clickrec_T *clicktab,
+ char_u *buf, int row, int col_start, int col_end, int tabnr);
// set pcurtab_row. don't redraw tabpanel.
#define TPLMODE_GET_CURTAB_ROW 0
@@ -135,6 +138,108 @@ tabpanel_leftcol(void)
return tpl_align == ALIGN_RIGHT ? 0 : tabpanel_width();
}
+/*
+ * Free previously resolved 'tabpanel' click regions.
+ */
+ static void
+tabpanel_free_click_regions(void)
+{
+ int n;
+
+ if (tabpanel_stl_click != NULL)
+ {
+ for (n = 0; n < tabpanel_stl_click_count; n++)
+ vim_free(tabpanel_stl_click[n].funcname);
+ VIM_CLEAR(tabpanel_stl_click);
+ }
+ tabpanel_stl_click_count = 0;
+}
+
+/*
+ * Convert click records produced by build_stl_str_hl() for one line of
+ * 'tabpanel' into screen-column based regions and append them to the global
+ * tabpanel_stl_click array. The caller keeps ownership of the funcname
+ * strings inside "clicktab" — this function makes its own copies.
+ */
+ static void
+tabpanel_append_click_regions(
+ stl_clickrec_T *clicktab,
+ char_u *buf,
+ int row,
+ int col_start,
+ int col_end,
+ int tabnr)
+{
+ int count = 0;
+ int n;
+ int base_col;
+ int acc_width = 0;
+ int max_w = col_end - col_start;
+ char_u *p;
+ char_u *cur_funcname = NULL;
+ int cur_minwid = 0;
+ int region_start_col;
+ stl_click_region_T *new_arr;
+ int limit;
+
+ if (clicktab == NULL)
+ return;
+
+ for (n = 0; clicktab[n].start != NULL; n++)
+ count++;
+ if (count == 0)
+ return;
+
+ base_col = (tpl_align == ALIGN_RIGHT ? topframe->fr_width : 0) + col_start;
+ region_start_col = base_col;
+
+ // Grow the global array to make room for up to "count" more regions
+ // (one close for each record plus a possible trailing region).
+ new_arr = vim_realloc(tabpanel_stl_click,
+ sizeof(stl_click_region_T) * (tabpanel_stl_click_count + count + 1));
+ if (new_arr == NULL)
+ return;
+ tabpanel_stl_click = new_arr;
+
+ p = buf;
+ for (n = 0; clicktab[n].start != NULL; n++)
+ {
+ acc_width += vim_strnsize(p, (int)(clicktab[n].start - p));
+ p = clicktab[n].start;
+ limit = acc_width < max_w ? acc_width : max_w;
+
+ if (cur_funcname != NULL)
+ {
+ stl_click_region_T *r =
+ &tabpanel_stl_click[tabpanel_stl_click_count];
+ r->row = row;
+ r->col_start = region_start_col;
+ r->col_end = base_col + limit;
+ r->funcname = vim_strsave(cur_funcname);
+ r->minwid = cur_minwid;
+ r->tabnr = tabnr;
+ tabpanel_stl_click_count++;
+ }
+
+ cur_funcname = clicktab[n].funcname;
+ cur_minwid = clicktab[n].minwid;
+ region_start_col = base_col + limit;
+ }
+
+ // Close the final region if it extends to the end.
+ if (cur_funcname != NULL)
+ {
+ stl_click_region_T *r = &tabpanel_stl_click[tabpanel_stl_click_count];
+ r->row = row;
+ r->col_start = region_start_col;
+ r->col_end = base_col + max_w;
+ r->funcname = vim_strsave(cur_funcname);
+ r->minwid = cur_minwid;
+ r->tabnr = tabnr;
+ tabpanel_stl_click_count++;
+ }
+}
+
/*
* draw the tabpanel.
*/
@@ -150,7 +255,13 @@ draw_tabpanel(void)
int is_right = tpl_align == ALIGN_RIGHT;
if (maxwidth == 0)
+ {
+ tabpanel_free_click_regions();
return;
+ }
+
+ // Discard old click regions — they'll be rebuilt during redraw below.
+ tabpanel_free_click_regions();
// Reset got_int to avoid build_stl_str_hl() isn't evaluated.
got_int = FALSE;
@@ -495,6 +606,7 @@ do_by_tplmode(
char_u buf[IOSIZE];
stl_hlrec_T *hltab;
stl_hlrec_T *tabtab;
+ stl_clickrec_T *clicktab = NULL;
if (args.maxrow <= row - args.offsetrow)
break;
@@ -508,13 +620,31 @@ do_by_tplmode(
(args.cwp, buf, sizeof(buf),
&usefmt, opt_name, opt_scope, TPL_FILLCHAR,
args.col_end - args.col_start, &hltab, &tabtab,
- NULL);
+ tplmode == TPLMODE_REDRAW ? &clicktab : NULL);
args.prow = &row;
args.pcol = &col;
draw_tabpanel_with_highlight(tplmode, buf, hltab, &args);
+ // Record any %[FuncName] click regions for this line once
+ // the text has been drawn. Only visible rows participate.
+ if (tplmode == TPLMODE_REDRAW && clicktab != NULL)
+ {
+ int screen_row = row - args.offsetrow;
+ int m;
+
+ if (screen_row >= 0 && screen_row < args.maxrow)
+ tabpanel_append_click_regions(clicktab, buf,
+ screen_row, args.col_start, args.col_end,
+ (int)v.vval.v_number);
+ // We took ownership of the click records — free the
+ // function names (matches the non-NULL clicktab path in
+ // build_stl_str_hl()).
+ for (m = 0; clicktab[m].start != NULL; m++)
+ vim_free(clicktab[m].funcname);
+ }
+
// Move to next line for %@
if (*usefmt != NUL)
{
diff --git a/src/testdir/test_tabpanel.vim b/src/testdir/test_tabpanel.vim
index 4bb7f39eb..7803a0eb4 100644
--- a/src/testdir/test_tabpanel.vim
+++ b/src/testdir/test_tabpanel.vim
@@ -249,6 +249,76 @@ function Test_tabpanel_mouse()
let &showtabline = save_showtabline
endfunc
+func g:TplClickTestFunc(info)
+ let g:tpl_click_info = a:info
+ return 0
+endfunc
+
+function Test_tabpanel_click_handler()
+ let save_mouse = &mouse
+ let save_stal = &showtabline
+ let save_stpl = &showtabpanel
+ let save_tpl = &tabpanel
+ let save_tplo = &tabpanelopt
+ set mouse=a
+ set showtabline=0
+ set showtabpanel=2
+ set tabpanelopt=columns:16
+ tabnew
+ tabnew
+
+ " Place two adjacent %[FuncName] regions on every tab label.
+ set tabpanel=%1[TplClickTestFunc][A]%[]%2[TplClickTestFunc][B]%[]
+ redraw!
+
+ " Click on [A] region in the first tab label (row 1).
+ call test_setmouse(1, 2)
+ call feedkeys("\<LeftMouse>\<LeftRelease>", 'xt')
+ call assert_true(exists('g:tpl_click_info'))
+ call assert_equal('l', g:tpl_click_info.button)
+ call assert_equal(1, g:tpl_click_info.nclicks)
+ call assert_equal(1, g:tpl_click_info.minwid)
+ call assert_equal(0, g:tpl_click_info.winid)
+ call assert_equal('tabpanel', g:tpl_click_info.area)
+ call assert_equal(1, g:tpl_click_info.tabnr)
+ unlet! g:tpl_click_info
+
+ " Click on [B] region in the second tab label (row 2).
+ call test_setmouse(2, 5)
+ call feedkeys("\<LeftMouse>\<LeftRelease>", 'xt')
+ call assert_true(exists('g:tpl_click_info'))
+ call assert_equal(2, g:tpl_click_info.minwid)
+ call assert_equal(2, g:tpl_click_info.tabnr)
+ unlet! g:tpl_click_info
+
+ " Middle click on [A] in tab 3.
+ call test_setmouse(3, 2)
+ call feedkeys("\<MiddleMouse>\<MiddleRelease>", 'xt')
+ call assert_true(exists('g:tpl_click_info'))
+ call assert_equal('m', g:tpl_click_info.button)
+ call assert_equal(1, g:tpl_click_info.minwid)
+ call assert_equal(3, g:tpl_click_info.tabnr)
+ unlet! g:tpl_click_info
+
+ " A click outside any region (but still in the panel) must not fire the
+ " callback, and should fall through to the normal tab selection.
+ set tabpanel=xxx%1[TplClickTestFunc][Y]%[]
+ redraw!
+ tabfirst
+ call test_setmouse(2, 1)
+ call feedkeys("\<LeftMouse>\<LeftRelease>", 'xt')
+ call assert_false(exists('g:tpl_click_info'))
+ call assert_equal(2, tabpagenr())
+
+ tabonly!
+ call s:reset()
+ let &tabpanel = save_tpl
+ let &tabpanelopt = save_tplo
+ let &showtabpanel = save_stpl
+ let &showtabline = save_stal
+ let &mouse = save_mouse
+endfunc
+
function Test_tabpanel_drawing()
CheckScreendump
diff --git a/src/version.c b/src/version.c
index b015465ed..2361deb7d 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 */
+/**/
+ 360,
/**/
359,
/**/